From 50ce96fabcc2f8bdb025cae0f96ceacbc82e9c26 Mon Sep 17 00:00:00 2001 From: Sylvie Date: Wed, 18 Dec 2024 11:22:35 -0600 Subject: [PATCH 01/29] split out live and pre live query pack queries (#1660) --- operations/template/logs.tf | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/operations/template/logs.tf b/operations/template/logs.tf index 90effe550..5ca51a8e4 100644 --- a/operations/template/logs.tf +++ b/operations/template/logs.tf @@ -47,14 +47,24 @@ resource "azurerm_log_analytics_query_pack" "application_logs_pack" { } } -resource "azurerm_log_analytics_query_pack_query" "application_logs" { - display_name = "TI's Raw Application Logs" - description = "View all TI's application logs in a structured format" +resource "azurerm_log_analytics_query_pack_query" "live_application_logs" { + display_name = "TI's Live Slot Raw Application Logs" + description = "View all TI's live slot application logs in a structured format" query_pack_id = azurerm_log_analytics_query_pack.application_logs_pack.id categories = ["applications"] - body = "AppServiceConsoleLogs | project JsonResult = parse_json(ResultDescription) | evaluate bag_unpack(JsonResult) | project-reorder ['@timestamp'], level, message" + body = "AppServiceConsoleLogs | where _ResourceId !contains 'pre-live' | project JsonResult = parse_json(ResultDescription) | evaluate bag_unpack(JsonResult) | project-reorder ['@timestamp'], level, message" +} + +resource "azurerm_log_analytics_query_pack_query" "prelive_application_logs" { + display_name = "TI's Pre-Live Slot Raw Application Logs" + description = "View all TI's pre-live slot application logs in a structured format" + + query_pack_id = azurerm_log_analytics_query_pack.application_logs_pack.id + categories = ["applications"] + + body = "AppServiceConsoleLogs | where _ResourceId contains 'pre-live' | project JsonResult = parse_json(ResultDescription) | evaluate bag_unpack(JsonResult) | project-reorder ['@timestamp'], level, message" } resource "azurerm_log_analytics_query_pack_query" "application_error_logs" { From e985e4ce1c11c08b6a67eed230ab3c26a4beebcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:15:27 +0000 Subject: [PATCH 02/29] Update dependency ch.qos.logback:logback-classic to v1.5.13 (#1661) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- shared/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/build.gradle b/shared/build.gradle index fc76e9bb3..3d90afc7b 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -18,7 +18,7 @@ dependencies { // logging implementation 'org.slf4j:slf4j-api:2.0.16' - implementation 'ch.qos.logback:logback-classic:1.5.12' + implementation 'ch.qos.logback:logback-classic:1.5.13' implementation 'net.logstash.logback:logstash-logback-encoder:8.0' //jackson From b34a2a19b6142215c3ea0d63f765e0c8ec31a480 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 5 Dec 2024 15:01:30 -0600 Subject: [PATCH 03/29] WIP - remove cx.5 patient identifier warnings --- .../custom/RemovePatientIdentifiers.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java index 524665d26..d49febfa1 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java @@ -5,6 +5,7 @@ import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Identifier; /** * Removes Assigning Authority (PID-3.4) and Identifier Type Code (PID-3.5) from Patient Identifier @@ -15,7 +16,26 @@ public class RemovePatientIdentifiers implements CustomFhirTransformation { @Override public void transform(HealthData resource, Map args) { Bundle bundle = (Bundle) resource.getUnderlyingData(); - HapiHelper.setPID3_4Value(bundle, ""); // remove PID.3-4 - HapiHelper.setPID3_5Value(bundle, ""); // remove PID.3-5 + + Identifier identifier = HapiHelper.getPID3Identifier(bundle); + if (identifier == null) { + return; + } + identifier.setAssigner(null); + + if (identifier.hasExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL)) { + identifier + .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) + .removeExtension(HapiHelper.EXTENSION_CX5_URL); + } + + if (identifier + .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) + .getExtension() + .isEmpty()) { + identifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + } + + identifier.setType(null); } } From 84874578cad8a16e1cd4f77ac45e955d2cccd6e3 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 5 Dec 2024 16:53:51 -0600 Subject: [PATCH 04/29] Move PID-3.5 removal to HapiHelper --- .../custom/RemovePatientIdentifiers.java | 22 +++------------- .../external/hapi/HapiHelper.java | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java index d49febfa1..7c686b911 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java @@ -17,25 +17,11 @@ public class RemovePatientIdentifiers implements CustomFhirTransformation { public void transform(HealthData resource, Map args) { Bundle bundle = (Bundle) resource.getUnderlyingData(); - Identifier identifier = HapiHelper.getPID3Identifier(bundle); - if (identifier == null) { + Identifier patientIdentifier = HapiHelper.getPID3Identifier(bundle); + if (patientIdentifier == null) { return; } - identifier.setAssigner(null); - - if (identifier.hasExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL)) { - identifier - .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) - .removeExtension(HapiHelper.EXTENSION_CX5_URL); - } - - if (identifier - .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) - .getExtension() - .isEmpty()) { - identifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); - } - - identifier.setType(null); + patientIdentifier.setAssigner(null); + HapiHelper.removePID3_5Value(bundle); } } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index 8ff7cb02f..f42e426b5 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -288,6 +288,31 @@ public static void setPID3_5Value(Bundle bundle, String value) { setCX5Value(identifier, value); } + public static void removePID3_5Value(Bundle bundle) { + Identifier patientIdentifier = getPID3Identifier(bundle); + + if (patientIdentifier == null) { + return; + } + + if (patientIdentifier.hasExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL)) { + patientIdentifier + .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) + .removeExtension(HapiHelper.EXTENSION_CX5_URL); + } + + // The cx-identifier extension can be removed if it has no more sub-extensions + if (patientIdentifier + .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) + .getExtension() + .isEmpty()) { + patientIdentifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + } + + // The PID-3.5 also appears in the type coding + patientIdentifier.setType(null); + } + // PID-5 - Patient Name public static Extension getPID5Extension(Bundle bundle) { Patient patient = getPIDPatient(bundle); From 450c8aa7519c3d3a6134b4ef29841b7cd436d19d Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 5 Dec 2024 17:05:11 -0600 Subject: [PATCH 05/29] Change parameter from bundle to patient indentifer --- .../transformation/custom/RemovePatientIdentifiers.java | 2 +- .../hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java index 7c686b911..74f4a806e 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java @@ -22,6 +22,6 @@ public void transform(HealthData resource, Map args) { return; } patientIdentifier.setAssigner(null); - HapiHelper.removePID3_5Value(bundle); + HapiHelper.removePID3_5Value(patientIdentifier); } } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index f42e426b5..5b0738547 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -288,9 +288,7 @@ public static void setPID3_5Value(Bundle bundle, String value) { setCX5Value(identifier, value); } - public static void removePID3_5Value(Bundle bundle) { - Identifier patientIdentifier = getPID3Identifier(bundle); - + public static void removePID3_5Value(Identifier patientIdentifier) { if (patientIdentifier == null) { return; } From 90ca46cca19e156d3d5a917260f8b3782018765b Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Fri, 6 Dec 2024 14:27:06 -0600 Subject: [PATCH 06/29] Add tests for PID-3.5 removal --- .../external/hapi/HapiHelper.java | 2 +- .../external/hapi/HapiHelperTest.groovy | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index 5b0738547..cce9bbf02 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -307,7 +307,7 @@ public static void removePID3_5Value(Identifier patientIdentifier) { patientIdentifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); } - // The PID-3.5 also appears in the type coding + // The PID-3.5 value also appears in the type coding patientIdentifier.setType(null); } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index b8ff46c6b..71b4c6870 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -477,6 +477,36 @@ class HapiHelperTest extends Specification { HapiFhirHelper.getPID3_5Value(bundle) == pid3_5 } + // PID-3.5 - Removing Identifier Type Code + def "null patientIdentifier remains null"() { + when: + Identifier patientIdentfier = null + HapiHelper.removePID3_5Value(patientIdentfier) + + then: + patientIdentifier == null + } + + def "removing of the patient assigning identifier clears extensions and type coding"() { + given: + def pid3_5 = "pid3_5" + def bundle = new Bundle() + def patientIdentifier = new Identifier() + + HapiFhirHelper.createPIDPatient(bundle) + HapiFhirHelper.setPID3Identifier(bundle, patientIdentifier) + HapiHelper.setPID3_5Value(bundle, pid3_5) + + patientIdentifier.type.addCoding(new Coding(code: pid3_5)) + + when: + HapiHelper.removePID3_5Value(patientIdentifier) + + then: + patientIdentifier.extension.isEmpty() + patientIdentifier.type.isEmpty() + } + // PID-5 - Patient Name def "patient name methods work as expected"() { given: From 73c2c5e62599c5c98c7393181a43373bb062c595 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Fri, 6 Dec 2024 17:23:19 -0600 Subject: [PATCH 07/29] Move creation of OBR-16 extension into HapiHelper method, method parameter refactor, fix logic for existence of practitioner --- .../CopyOrcOrderProviderToObrOrderProvider.java | 9 ++------- ...rcOrderProviderToObrOrderProviderTest.groovy | 13 ++++++++++--- .../external/hapi/HapiHelper.java | 14 ++++++++++---- .../external/hapi/HapiHelperTest.groovy | 17 ++++++++--------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java index 1c73395a2..e3040fdd2 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java @@ -28,16 +28,11 @@ public void transform(HealthData resource, Map args) { return; } - // Extract or create the OBR-16 extension from the ServiceRequest + // Extract or create the OBR extension from the ServiceRequest Extension obrExtension = HapiHelper.ensureExtensionExists(serviceRequest, HapiHelper.EXTENSION_OBR_URL); - // Extract or create the OBR-16 data type extension - Extension obr16Extension = - HapiHelper.ensureSubExtensionExists( - obrExtension, HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()); - // Set the ORC-12 Practitioner in the OBR-16 extension - HapiHelper.setOBR16WithPractitioner(obr16Extension, practitionerRole); + HapiHelper.setOBR16WithPractitioner(obrExtension, practitionerRole); } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index eb74ed5b6..b619d6001 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -7,6 +7,7 @@ import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.DiagnosticReport +import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.ServiceRequest @@ -214,6 +215,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ } void evaluateObr16IsNull(ServiceRequest serviceRequest) { + assert getObr16Extension(serviceRequest) == null assert getObr16ExtensionPractitioner(serviceRequest) == null } @@ -235,12 +237,17 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ assert codingSystem == null || codingSystem[0]?.code == expectedIdentifierTypeCode } + Extension getObr16Extension(serviceRequest) { + def obrExtension = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = obrExtension.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + return obr16Extension + } + Practitioner getObr16ExtensionPractitioner (serviceRequest) { def resource try { - def extensionByUrl1 = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_OBR_URL) - def extensionByUrl2 = extensionByUrl1.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) - def value = extensionByUrl2.value + def obr16Extension = getObr16Extension(serviceRequest) + def value = obr16Extension.value resource = value.getResource() return resource } catch(Exception ignored) { diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index cce9bbf02..a6d06715a 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -268,6 +268,9 @@ public static Identifier getPID3_4Identifier(Bundle bundle) { return null; } Organization organization = (Organization) identifier.getAssigner().getResource(); + if (organization == null) { + return null; + } return organization.getIdentifierFirstRep(); } @@ -523,13 +526,16 @@ public static String getOBR4_1Value(ServiceRequest serviceRequest) { } // OBR16 - Ordering Provider - - // OBR16 - public static void setOBR16WithPractitioner( - Extension obr16Extension, PractitionerRole practitionerRole) { - if (practitionerRole == null) { + Extension obrExtension, PractitionerRole practitionerRole) { + if (practitionerRole == null || !practitionerRole.getPractitioner().hasReference()) { return; } + + Extension obr16Extension = + HapiHelper.ensureSubExtensionExists( + obrExtension, HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()); + obr16Extension.setValue(practitionerRole.getPractitioner()); } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 71b4c6870..fc69b039f 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -480,8 +480,8 @@ class HapiHelperTest extends Specification { // PID-3.5 - Removing Identifier Type Code def "null patientIdentifier remains null"() { when: - Identifier patientIdentfier = null - HapiHelper.removePID3_5Value(patientIdentfier) + Identifier patientIdentifier = null + HapiHelper.removePID3_5Value(patientIdentifier) then: patientIdentifier == null @@ -893,21 +893,19 @@ class HapiHelperTest extends Specification { def "setOBR16WithPractitioner sets the expected value on an extension"() { given: - def ext = new Extension() + Extension obrExtension = new Extension() def role = new PractitionerRole() def practitioner = new Practitioner() practitioner.setId("test123") def ref = new Reference(practitioner.getId()) role.setPractitioner(ref) - expect: - ext.getValue() == null - when: - HapiHelper.setOBR16WithPractitioner(ext, role) + HapiHelper.setOBR16WithPractitioner(obrExtension, role) then: - ext.getValue().getReference() == "test123" + def obr16Extension = obrExtension.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obr16Extension.getValue().getReference() == "test123" } def "setOBR16WithPractitioner does nothing if the provided PractitionerRole is null"() { @@ -922,7 +920,8 @@ class HapiHelperTest extends Specification { HapiHelper.setOBR16WithPractitioner(ext, role) then: - ext.getValue() == null + def obr16Extension = ext.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obr16Extension == null } def "urlForCodeType should return expected values"() { From 6793247f2bc73425fa11d664ef799d1156e947b0 Mon Sep 17 00:00:00 2001 From: Tiffini Johnson Date: Thu, 12 Dec 2024 13:54:30 -0500 Subject: [PATCH 08/29] Unit Test for removal of PID-5.7 --- .../RemovePatientNameTypeCodeTest.groovy | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy index 53927b19f..5bead1d75 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy @@ -20,7 +20,7 @@ class RemovePatientNameTypeCodeTest extends Specification { transformClass = new RemovePatientNameTypeCode() } - def "remove PID.5-7 from Bundle"() { + def "remove PID.5-7 from Bundle - old"() { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") def bundle = fhirResource.getUnderlyingData() as Bundle @@ -36,6 +36,30 @@ class RemovePatientNameTypeCodeTest extends Specification { HapiFhirHelper.getPID5_7Value(bundle) == null } + def "remove PID.5-7 from Bundle"() { + given: + def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") + def bundle = fhirResource.getUnderlyingData() as Bundle + def pid_5_initial = HapiHelper.getPID5Extension(bundle) + def xpn_7_initial = pid_5_initial.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) + def patientName = HapiHelper.getPIDPatient(bundle).getNameFirstRep() + def patientNameUse = patientName.getUse() + + expect: + xpn_7_initial != null + patientNameUse.toString() == "OFFICIAL" + + when: + transformClass.transform(fhirResource, null) + + then: + def pid_5_transformed = HapiHelper.getPID5Extension(bundle) + def xpn_7_transformed = pid_5_transformed.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) + xpn_7_transformed == null + + !patientName.hasUse() + } + def "don't throw exception if patient resource not present"() { given: def bundle = new Bundle() From 7e1377a0ea899e4829ff9c31588f9bee66d7e098 Mon Sep 17 00:00:00 2001 From: Tiffini Johnson Date: Thu, 12 Dec 2024 13:55:59 -0500 Subject: [PATCH 09/29] Added method for full removal of PID5.7 and name use Co-Authored-By: Joel Biskie <84460447+jbiskie@users.noreply.github.com> Co-Authored-By: Jeremy Rosenfeld <10262289+JeremyIR@users.noreply.github.com> --- .../custom/RemovePatientNameTypeCode.java | 4 +--- .../external/hapi/HapiHelper.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java index 5cf7d4ce2..a7244ba6d 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCode.java @@ -12,8 +12,6 @@ public class RemovePatientNameTypeCode implements CustomFhirTransformation { @Override public void transform(final HealthData resource, final Map args) { Bundle bundle = (Bundle) resource.getUnderlyingData(); - // Need to set the value for extension to empty instead of removing the extension, - // otherwise RS will set its own value in its place - HapiHelper.setPID5_7ExtensionValue(bundle, null); + HapiHelper.removePID5_7Value(bundle); } } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index a6d06715a..192a43d6e 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -333,6 +333,20 @@ public static void setPID5_7ExtensionValue(Bundle bundle, String value) { } } + public static void removePID5_7Value(Bundle bundle) { + Extension pid5Extension = HapiHelper.getPID5Extension(bundle); + if (pid5Extension == null) { + return; + } + Extension xpn7Extension = pid5Extension.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL); + if (xpn7Extension != null) { + pid5Extension.removeExtension(HapiHelper.EXTENSION_XPN7_URL); + } + + HumanName patientName = HapiHelper.getPIDPatient(bundle).getNameFirstRep(); + patientName.setUse(null); + } + // ORC - Common Order // Diagnostic Report From 3c4a70a0fb1e5cfc8e92bce8d321f107a7e0cf97 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 12 Dec 2024 13:29:20 -0600 Subject: [PATCH 10/29] Remove old unit test, update input file for PID-5.7 test Co-authored-by: tjohnson@flexion.us Co-authored-by: jrosenfeld@flexion.us --- .../RemovePatientNameTypeCodeTest.groovy | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy index 5bead1d75..e58cd5982 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy @@ -20,25 +20,9 @@ class RemovePatientNameTypeCodeTest extends Specification { transformClass = new RemovePatientNameTypeCode() } - def "remove PID.5-7 from Bundle - old"() { - given: - def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") - def bundle = fhirResource.getUnderlyingData() as Bundle - def pid5_7 = HapiFhirHelper.getPID5_7Value(bundle) - - expect: - pid5_7 != null - - when: - transformClass.transform(fhirResource, null) - - then: - HapiFhirHelper.getPID5_7Value(bundle) == null - } - def "remove PID.5-7 from Bundle"() { given: - def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/002_CA_ORU_R01_initial_translation.fhir") + def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/007_CA_ORU_R01_CDPH_produced_UCSD2024-07-11-16-02-17-749_1_hl7_translation.fhir") def bundle = fhirResource.getUnderlyingData() as Bundle def pid_5_initial = HapiHelper.getPID5Extension(bundle) def xpn_7_initial = pid_5_initial.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) From e7acb921a14622f4303464040378d7e2e26691f5 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 12 Dec 2024 15:22:35 -0600 Subject: [PATCH 11/29] Improve readability and null checks in removePID3_5Value and add null check in getObr16ExtensionPractitioner --- ...OrderProviderToObrOrderProviderTest.groovy | 13 ++++------- .../external/hapi/HapiHelper.java | 22 +++++++++---------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index b619d6001..075b844f5 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -244,16 +244,11 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ } Practitioner getObr16ExtensionPractitioner (serviceRequest) { - def resource - try { - def obr16Extension = getObr16Extension(serviceRequest) - def value = obr16Extension.value - resource = value.getResource() - return resource - } catch(Exception ignored) { - resource = null - return resource + def obr16Extension = getObr16Extension(serviceRequest) + if (obr16Extension == null) { + return null } + return obr16Extension.value.getResource() } Practitioner getOrc12ExtensionPractitioner(Bundle bundle) { diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index 192a43d6e..abd1754f0 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -296,22 +296,22 @@ public static void removePID3_5Value(Identifier patientIdentifier) { return; } - if (patientIdentifier.hasExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL)) { - patientIdentifier - .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) - .removeExtension(HapiHelper.EXTENSION_CX5_URL); + patientIdentifier.setType(null); + + Extension cxExtension = + patientIdentifier.getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + if (cxExtension == null) { + return; + } + + if (cxExtension.hasExtension(HapiHelper.EXTENSION_CX5_URL)) { + cxExtension.removeExtension(HapiHelper.EXTENSION_CX5_URL); } // The cx-identifier extension can be removed if it has no more sub-extensions - if (patientIdentifier - .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) - .getExtension() - .isEmpty()) { + if (cxExtension.getExtension().isEmpty()) { patientIdentifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); } - - // The PID-3.5 value also appears in the type coding - patientIdentifier.setType(null); } // PID-5 - Patient Name From fb3b0d8db4d74e180d434376d599ec60b58ae3d2 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 12 Dec 2024 15:52:47 -0600 Subject: [PATCH 12/29] Fix possible null value for getPIDPatient Co-authored-by: tjohnson7021 Co-authored-by: jeremyir --- .../trustedintermediary/external/hapi/HapiHelper.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index abd1754f0..bee351dea 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -334,6 +334,13 @@ public static void setPID5_7ExtensionValue(Bundle bundle, String value) { } public static void removePID5_7Value(Bundle bundle) { + Patient patient = HapiHelper.getPIDPatient(bundle); + if (patient == null) { + return; + } + HumanName patientName = patient.getNameFirstRep(); + patientName.setUse(null); + Extension pid5Extension = HapiHelper.getPID5Extension(bundle); if (pid5Extension == null) { return; @@ -342,9 +349,6 @@ public static void removePID5_7Value(Bundle bundle) { if (xpn7Extension != null) { pid5Extension.removeExtension(HapiHelper.EXTENSION_XPN7_URL); } - - HumanName patientName = HapiHelper.getPIDPatient(bundle).getNameFirstRep(); - patientName.setUse(null); } // ORC - Common Order From a46d055b0fa6a21851fd7e685d724ec919f03c22 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Thu, 12 Dec 2024 17:01:31 -0600 Subject: [PATCH 13/29] Add unit tests for removePID5_7Value Co-authored-by: jeremyir --- .../external/hapi/HapiHelperTest.groovy | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index fc69b039f..0f422e296 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -479,14 +479,30 @@ class HapiHelperTest extends Specification { // PID-3.5 - Removing Identifier Type Code def "null patientIdentifier remains null"() { - when: + given: Identifier patientIdentifier = null + + when: HapiHelper.removePID3_5Value(patientIdentifier) then: patientIdentifier == null } + def "patientIdentifier with no extensions leaves extensions as-is"() { + given: + Identifier patientIdentifier = new Identifier() + + expect: + !patientIdentifier.hasExtension() + + when: + HapiHelper.removePID3_5Value(patientIdentifier) + + then: + !patientIdentifier.hasExtension() + } + def "removing of the patient assigning identifier clears extensions and type coding"() { given: def pid3_5 = "pid3_5" @@ -561,6 +577,45 @@ class HapiHelperTest extends Specification { HapiFhirHelper.getPID5_7Value(bundle) == null } + // PID-5.7 - Removing Name Type Code + def "bundle without patient remains empty"() { + given: + def bundle = new Bundle() + + when: + HapiHelper.removePID5_7Value(bundle) + + then: + bundle.getEntry().size() == 0 + } + + def "bundle with patient but no extensions remains empty"() { + given: + def bundle = new Bundle() + HapiFhirHelper.createPIDPatient(bundle) + + when: + HapiHelper.removePID5_7Value(bundle) + + then: + HapiHelper.getPID5Extension(bundle) == null + } + + def "bundle with patient and pid5 extensions but no xpn7 extension remains empty"() { + given: + def bundle = new Bundle() + HapiFhirHelper.createPIDPatient(bundle) + HapiFhirHelper.setPID5Extension(bundle) + + when: + HapiHelper.removePID5_7Value(bundle) + + then: + Extension pid5Extension = HapiHelper.getPID5Extension(bundle) + !pid5Extension.hasExtension() + } + + // ORC-2 - Placer Order Number def "orc-2.1 methods work as expected"() { given: From d16eff0aa77dafd72d317068c89d9fc27ed21d77 Mon Sep 17 00:00:00 2001 From: Jeremy Rosenfeld Date: Fri, 13 Dec 2024 11:03:40 -0800 Subject: [PATCH 14/29] add additional test coverage for removePID3_5Value method --- .../external/hapi/HapiHelperTest.groovy | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 0f422e296..872728c3f 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -523,6 +523,27 @@ class HapiHelperTest extends Specification { patientIdentifier.type.isEmpty() } + def "removing patient identifier with non-cx5 url extension doesnt clears extensions"() { + given: + def bundle = new Bundle() + def patientIdentifier = new Identifier() + + HapiFhirHelper.createPIDPatient(bundle) + HapiFhirHelper.setPID3Identifier(bundle, patientIdentifier) + patientIdentifier.addExtension().setUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + patientIdentifier + .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) + .addExtension() + .setUrl(HapiHelper.EXTENSION_XON10_URL); + + when: + HapiHelper.removePID3_5Value(patientIdentifier) + + then: + patientIdentifier.hasExtension() + } + + // PID-5 - Patient Name def "patient name methods work as expected"() { given: From dc806510a658c2fb957dd9dcd21f8c99d28d762b Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Fri, 13 Dec 2024 14:44:46 -0600 Subject: [PATCH 15/29] Remove obvious comments; clean up test --- .../custom/CopyOrcOrderProviderToObrOrderProvider.java | 3 --- .../custom/RemovePatientNameTypeCodeTest.groovy | 10 +++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java index e3040fdd2..417d78491 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProvider.java @@ -28,11 +28,8 @@ public void transform(HealthData resource, Map args) { return; } - // Extract or create the OBR extension from the ServiceRequest Extension obrExtension = HapiHelper.ensureExtensionExists(serviceRequest, HapiHelper.EXTENSION_OBR_URL); - - // Set the ORC-12 Practitioner in the OBR-16 extension HapiHelper.setOBR16WithPractitioner(obrExtension, practitionerRole); } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy index e58cd5982..b5780983e 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientNameTypeCodeTest.groovy @@ -24,23 +24,19 @@ class RemovePatientNameTypeCodeTest extends Specification { given: def fhirResource = ExamplesHelper.getExampleFhirResource("../CA/007_CA_ORU_R01_CDPH_produced_UCSD2024-07-11-16-02-17-749_1_hl7_translation.fhir") def bundle = fhirResource.getUnderlyingData() as Bundle - def pid_5_initial = HapiHelper.getPID5Extension(bundle) - def xpn_7_initial = pid_5_initial.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) + def pid5Extension = HapiHelper.getPID5Extension(bundle) def patientName = HapiHelper.getPIDPatient(bundle).getNameFirstRep() def patientNameUse = patientName.getUse() expect: - xpn_7_initial != null + pid5Extension.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) != null patientNameUse.toString() == "OFFICIAL" when: transformClass.transform(fhirResource, null) then: - def pid_5_transformed = HapiHelper.getPID5Extension(bundle) - def xpn_7_transformed = pid_5_transformed.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) - xpn_7_transformed == null - + pid5Extension.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) == null !patientName.hasUse() } From 630ff617ed548b050fa4e0771c5a6850a6a003f6 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Mon, 16 Dec 2024 16:19:03 -0600 Subject: [PATCH 16/29] Refactor the clearing of patient identifier assigner into setPID3_4Value, refactor PID-3.4 testing into discrete unit tests --- .../custom/RemovePatientIdentifiers.java | 9 +-- .../custom/RemovePatientIdentifierTest.groovy | 4 +- .../external/hapi/HapiHelper.java | 18 ++++-- .../external/hapi/HapiHelperTest.groovy | 64 ++++++++++++++----- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java index 74f4a806e..111a5a523 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifiers.java @@ -5,7 +5,6 @@ import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; import java.util.Map; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Identifier; /** * Removes Assigning Authority (PID-3.4) and Identifier Type Code (PID-3.5) from Patient Identifier @@ -17,11 +16,7 @@ public class RemovePatientIdentifiers implements CustomFhirTransformation { public void transform(HealthData resource, Map args) { Bundle bundle = (Bundle) resource.getUnderlyingData(); - Identifier patientIdentifier = HapiHelper.getPID3Identifier(bundle); - if (patientIdentifier == null) { - return; - } - patientIdentifier.setAssigner(null); - HapiHelper.removePID3_5Value(patientIdentifier); + HapiHelper.setPID3_4Value(bundle, null); + HapiHelper.removePID3_5Value(bundle); } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy index e1920f2c6..ef163ffbe 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemovePatientIdentifierTest.groovy @@ -33,10 +33,10 @@ class RemovePatientIdentifierTest extends Specification { when: transformClass.transform(fhirResource, null) - def actualPid3_4 = HapiFhirHelper.getPID3_4Value(bundle) - def actualPid3_5 = HapiFhirHelper.getPID3_5Value(bundle) then: + def actualPid3_4 = HapiFhirHelper.getPID3_4Value(bundle) + def actualPid3_5 = HapiFhirHelper.getPID3_5Value(bundle) actualPid3_4 == null || actualPid3_4.isEmpty() actualPid3_5 == null || actualPid3_5.isEmpty() } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index bee351dea..b3219f0e6 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -275,11 +275,20 @@ public static Identifier getPID3_4Identifier(Bundle bundle) { } public static void setPID3_4Value(Bundle bundle, String value) { - Identifier identifier = getPID3_4Identifier(bundle); - if (identifier == null) { + if (value == null) { + Identifier pid3Identifier = getPID3Identifier(bundle); + if (pid3Identifier == null) { + return; + } + pid3Identifier.setAssigner(null); return; } - identifier.setValue(value); + + Identifier pid3_4Identifier = getPID3_4Identifier(bundle); + if (pid3_4Identifier == null) { + return; + } + pid3_4Identifier.setValue(value); } // PID-3.5 - Identifier Type Code @@ -291,7 +300,8 @@ public static void setPID3_5Value(Bundle bundle, String value) { setCX5Value(identifier, value); } - public static void removePID3_5Value(Identifier patientIdentifier) { + public static void removePID3_5Value(Bundle bundle) { + Identifier patientIdentifier = HapiHelper.getPID3Identifier(bundle); if (patientIdentifier == null) { return; } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 872728c3f..3175d670b 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -430,30 +430,58 @@ class HapiHelperTest extends Specification { } // PID-3.4 - Assigning Authority - def "patient assigning authority methods work as expected"() { + def "getPID3_4Identifier returns null when bundle is empty"() { given: - def pid3_4 = "pid3_4" - - when: def bundle = new Bundle() - then: + expect: HapiHelper.getPID3_4Identifier(bundle) == null + } + + def "setPID3_4Value does not modify bundle without PID-3 identifier"() { + given: + def pid3_4Value = "pid3_4" + def bundle = new Bundle() when: - HapiHelper.setPID3_4Value(bundle, pid3_4) + HapiHelper.setPID3_4Value(bundle, pid3_4Value) then: HapiFhirHelper.getPID3_4Value(bundle) == null + } - when: + def "setPID3_4Value updates correctly when patient has a PID-3 identifier"() { + given: + def pid3_4Value = "pid3_4" + def bundle = new Bundle() HapiFhirHelper.createPIDPatient(bundle) HapiFhirHelper.setPID3Identifier(bundle, new Identifier()) HapiFhirHelper.setPID3_4Identifier(bundle, new Identifier()) - HapiHelper.setPID3_4Value(bundle, pid3_4) + + when: + HapiHelper.setPID3_4Value(bundle, pid3_4Value) then: - HapiFhirHelper.getPID3_4Value(bundle) == pid3_4 + HapiFhirHelper.getPID3_4Value(bundle) == pid3_4Value + } + + def "setPID3_4Value updates correctly when patient has a PID-3 identifier and the new value is null"() { + given: + def bundle = new Bundle() + HapiFhirHelper.createPIDPatient(bundle) + + def pid3Identifier = new Identifier() + HapiFhirHelper.setPID3Identifier(bundle, pid3Identifier) + + def pid3_4Value = "pid3_4" + HapiFhirHelper.setPID3_4Identifier(bundle, new Identifier()) + HapiHelper.setPID3_4Value(bundle, pid3_4Value) + + when: + HapiHelper.setPID3_4Value(bundle, null) + + then: + !pid3Identifier.hasAssigner() } // PID-3.5 - Assigning Identifier Type Code @@ -478,26 +506,28 @@ class HapiHelperTest extends Specification { } // PID-3.5 - Removing Identifier Type Code - def "null patientIdentifier remains null"() { + def "Bundle with no patient identifier does not change"() { given: - Identifier patientIdentifier = null + Bundle bundle = new Bundle() when: - HapiHelper.removePID3_5Value(patientIdentifier) + HapiHelper.removePID3_5Value(bundle) then: - patientIdentifier == null + !bundle.hasEntry() } def "patientIdentifier with no extensions leaves extensions as-is"() { given: + Bundle bundle = new Bundle() Identifier patientIdentifier = new Identifier() + HapiFhirHelper.setPID3Identifier(bundle, patientIdentifier) expect: !patientIdentifier.hasExtension() when: - HapiHelper.removePID3_5Value(patientIdentifier) + HapiHelper.removePID3_5Value(bundle) then: !patientIdentifier.hasExtension() @@ -516,14 +546,14 @@ class HapiHelperTest extends Specification { patientIdentifier.type.addCoding(new Coding(code: pid3_5)) when: - HapiHelper.removePID3_5Value(patientIdentifier) + HapiHelper.removePID3_5Value(bundle) then: patientIdentifier.extension.isEmpty() patientIdentifier.type.isEmpty() } - def "removing patient identifier with non-cx5 url extension doesnt clears extensions"() { + def "removing patient identifier with non-cx5 url extension does not clear extensions"() { given: def bundle = new Bundle() def patientIdentifier = new Identifier() @@ -537,7 +567,7 @@ class HapiHelperTest extends Specification { .setUrl(HapiHelper.EXTENSION_XON10_URL); when: - HapiHelper.removePID3_5Value(patientIdentifier) + HapiHelper.removePID3_5Value(bundle) then: patientIdentifier.hasExtension() From 0e9a35e04dedf481c8daa11576fad1a6a7aa6f58 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Tue, 17 Dec 2024 15:35:28 -0600 Subject: [PATCH 17/29] Refactor getObr16Extension to HapiHelper, add unit tests --- ...OrderProviderToObrOrderProviderTest.groovy | 10 +---- .../external/hapi/HapiHelper.java | 8 ++++ .../external/hapi/HapiHelperTest.groovy | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index 075b844f5..2a7f48c86 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -215,7 +215,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ } void evaluateObr16IsNull(ServiceRequest serviceRequest) { - assert getObr16Extension(serviceRequest) == null + assert HapiHelper.getObr16Extension(serviceRequest) == null assert getObr16ExtensionPractitioner(serviceRequest) == null } @@ -237,14 +237,8 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ assert codingSystem == null || codingSystem[0]?.code == expectedIdentifierTypeCode } - Extension getObr16Extension(serviceRequest) { - def obrExtension = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_OBR_URL) - def obr16Extension = obrExtension.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) - return obr16Extension - } - Practitioner getObr16ExtensionPractitioner (serviceRequest) { - def obr16Extension = getObr16Extension(serviceRequest) + def obr16Extension = HapiHelper.getObr16Extension(serviceRequest) if (obr16Extension == null) { return null } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index b3219f0e6..46b8e9f82 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -567,6 +567,14 @@ public static void setOBR16WithPractitioner( obr16Extension.setValue(practitionerRole.getPractitioner()); } + public static Extension getObr16Extension(ServiceRequest serviceRequest) { + Extension obrExtension = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_OBR_URL); + if (obrExtension == null) { + return null; + } + return obrExtension.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()); + } + /** * Ensures that the extension exists for a given serviceRequest. If the extension does not * exist, it will create it. diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 3175d670b..ea5a232cc 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -839,6 +839,46 @@ class HapiHelperTest extends Specification { HapiHelper.getOBR4_1Value(sr) == null } + // OBR-16 - Ordering Provider + def "getObr16Extension returns the extension if present"() { + given: + def serviceRequest = new ServiceRequest() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = new Extension(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obrExtension.addExtension(obr16Extension) + serviceRequest.addExtension(obrExtension) + + when: + def result = HapiHelper.getObr16Extension(serviceRequest) + + then: + result == obr16Extension + } + + def "getObr16Extension returns null when no OBR extension is present"() { + given: + def serviceRequest = new ServiceRequest() + + when: + def result = HapiHelper.getObr16Extension(serviceRequest) + + then: + result == null + } + + def "getObr16Extension returns null when OBR extension does not contain OBR-16"() { + given: + def serviceRequest = new ServiceRequest() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + serviceRequest.addExtension(obrExtension) + + when: + def result = HapiHelper.getObr16Extension(serviceRequest) + + then: + result == null + } + def "ensureExtensionExists returns extension if it exists"() { given: def serviceRequest = new ServiceRequest() From 1b0f8c73cb57423a1ef53103e0c9cebaec3483fa Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Tue, 17 Dec 2024 16:57:09 -0600 Subject: [PATCH 18/29] Refactor getObr16ExtensionPractitioner into HapiHelper, add unit tests --- ...OrderProviderToObrOrderProviderTest.groovy | 14 +--- .../external/hapi/HapiHelper.java | 66 ++++++++------- .../external/hapi/HapiHelperTest.groovy | 80 +++++++++++++++++++ 3 files changed, 120 insertions(+), 40 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index 2a7f48c86..cc983762f 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -120,7 +120,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ evaluateOrc12Values(bundle, EXPECTED_NPI, EXPECTED_FIRST_NAME, EXPECTED_LAST_NAME, EXPECTED_NAME_TYPE_CODE, EXPECTED_IDENTIFIER_TYPE_CODE) // OBR16 should not exist initially - def obr16Practitioner = getObr16ExtensionPractitioner(serviceRequest) + def obr16Practitioner = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) obr16Practitioner == null when: @@ -216,7 +216,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ void evaluateObr16IsNull(ServiceRequest serviceRequest) { assert HapiHelper.getObr16Extension(serviceRequest) == null - assert getObr16ExtensionPractitioner(serviceRequest) == null + assert HapiHelper.getObr16ExtensionPractitioner(serviceRequest) == null } void evaluateObr16Values( @@ -226,7 +226,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ String expectedLastName, String expectedNameTypeCode, String expectedIdentifierTypeCode) { - def practitioner = getObr16ExtensionPractitioner(serviceRequest) + def practitioner = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) def xcnExtension = practitioner.getExtensionByUrl(PRACTITIONER_EXTENSION_URL) assert practitioner.identifier[0]?.value == expectedNpi @@ -237,14 +237,6 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ assert codingSystem == null || codingSystem[0]?.code == expectedIdentifierTypeCode } - Practitioner getObr16ExtensionPractitioner (serviceRequest) { - def obr16Extension = HapiHelper.getObr16Extension(serviceRequest) - if (obr16Extension == null) { - return null - } - return obr16Extension.value.getResource() - } - Practitioner getOrc12ExtensionPractitioner(Bundle bundle) { def diagnosticReport = HapiHelper.getDiagnosticReport(bundle) def serviceRequest = HapiHelper.getServiceRequest(diagnosticReport) diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index 46b8e9f82..bef4b214a 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ServiceRequest; import org.hl7.fhir.r4.model.StringType; @@ -301,26 +302,25 @@ public static void setPID3_5Value(Bundle bundle, String value) { } public static void removePID3_5Value(Bundle bundle) { - Identifier patientIdentifier = HapiHelper.getPID3Identifier(bundle); + Identifier patientIdentifier = getPID3Identifier(bundle); if (patientIdentifier == null) { return; } patientIdentifier.setType(null); - Extension cxExtension = - patientIdentifier.getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + Extension cxExtension = patientIdentifier.getExtensionByUrl(EXTENSION_CX_IDENTIFIER_URL); if (cxExtension == null) { return; } - if (cxExtension.hasExtension(HapiHelper.EXTENSION_CX5_URL)) { - cxExtension.removeExtension(HapiHelper.EXTENSION_CX5_URL); + if (cxExtension.hasExtension(EXTENSION_CX5_URL)) { + cxExtension.removeExtension(EXTENSION_CX5_URL); } // The cx-identifier extension can be removed if it has no more sub-extensions if (cxExtension.getExtension().isEmpty()) { - patientIdentifier.removeExtension(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + patientIdentifier.removeExtension(EXTENSION_CX_IDENTIFIER_URL); } } @@ -331,33 +331,31 @@ public static Extension getPID5Extension(Bundle bundle) { return null; } HumanName name = patient.getNameFirstRep(); - return name.getExtensionByUrl(HapiHelper.EXTENSION_XPN_HUMAN_NAME_URL); + return name.getExtensionByUrl(EXTENSION_XPN_HUMAN_NAME_URL); } public static void setPID5_7ExtensionValue(Bundle bundle, String value) { Extension extension = getPID5Extension(bundle); - if (extension != null && extension.hasExtension(HapiHelper.EXTENSION_XPN7_URL)) { - extension - .getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL) - .setValue(new StringType(value)); + if (extension != null && extension.hasExtension(EXTENSION_XPN7_URL)) { + extension.getExtensionByUrl(EXTENSION_XPN7_URL).setValue(new StringType(value)); } } public static void removePID5_7Value(Bundle bundle) { - Patient patient = HapiHelper.getPIDPatient(bundle); + Patient patient = getPIDPatient(bundle); if (patient == null) { return; } HumanName patientName = patient.getNameFirstRep(); patientName.setUse(null); - Extension pid5Extension = HapiHelper.getPID5Extension(bundle); + Extension pid5Extension = getPID5Extension(bundle); if (pid5Extension == null) { return; } - Extension xpn7Extension = pid5Extension.getExtensionByUrl(HapiHelper.EXTENSION_XPN7_URL); + Extension xpn7Extension = pid5Extension.getExtensionByUrl(EXTENSION_XPN7_URL); if (xpn7Extension != null) { - pid5Extension.removeExtension(HapiHelper.EXTENSION_XPN7_URL); + pid5Extension.removeExtension(EXTENSION_XPN7_URL); } } @@ -561,18 +559,31 @@ public static void setOBR16WithPractitioner( } Extension obr16Extension = - HapiHelper.ensureSubExtensionExists( - obrExtension, HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()); + ensureSubExtensionExists(obrExtension, EXTENSION_OBR16_DATA_TYPE.toString()); obr16Extension.setValue(practitionerRole.getPractitioner()); } public static Extension getObr16Extension(ServiceRequest serviceRequest) { - Extension obrExtension = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_OBR_URL); + Extension obrExtension = serviceRequest.getExtensionByUrl(EXTENSION_OBR_URL); if (obrExtension == null) { return null; } - return obrExtension.getExtensionByUrl(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()); + return obrExtension.getExtensionByUrl(EXTENSION_OBR16_DATA_TYPE.toString()); + } + + public static Practitioner getObr16ExtensionPractitioner(ServiceRequest serviceRequest) { + Extension obr16Extension = getObr16Extension(serviceRequest); + if (obr16Extension == null || obr16Extension.getValue() == null) { + return null; + } + + if (obr16Extension.getValue() instanceof Reference reference) { + if (reference.getResource() instanceof Practitioner practitioner) { + return practitioner; + } + } + return null; } /** @@ -743,9 +754,9 @@ public static void removeHl7FieldIdentifier(List identifiers, String public static String urlForCodeType(String code) { return switch (code) { - case HapiHelper.LOINC_CODE -> HapiHelper.LOINC_URL; - case HapiHelper.PLT_CODE -> null; - default -> HapiHelper.LOCAL_CODE_URL; + case LOINC_CODE -> LOINC_URL; + case PLT_CODE -> null; + default -> LOCAL_CODE_URL; }; } @@ -760,22 +771,19 @@ public static String urlForCodeType(String code) { */ public static boolean hasDefinedCoding( Coding coding, String codingExt, String codingSystemExt) { - var codingExtMatch = - hasMatchingCodingExtension(coding, HapiHelper.EXTENSION_CWE_CODING, codingExt); + var codingExtMatch = hasMatchingCodingExtension(coding, EXTENSION_CWE_CODING, codingExt); var codingSystemExtMatch = - hasMatchingCodingExtension( - coding, HapiHelper.EXTENSION_CODING_SYSTEM, codingSystemExt); + hasMatchingCodingExtension(coding, EXTENSION_CODING_SYSTEM, codingSystemExt); return codingExtMatch && codingSystemExtMatch; } private static boolean hasMatchingCodingExtension( Coding coding, String extensionUrl, String valueToMatch) { - if (!HapiHelper.hasCodingExtensionWithUrl(coding, extensionUrl)) { + if (!hasCodingExtensionWithUrl(coding, extensionUrl)) { return false; } - var extensionValue = - HapiHelper.getCodingExtensionByUrl(coding, extensionUrl).getValue().toString(); + var extensionValue = getCodingExtensionByUrl(coding, extensionUrl).getValue().toString(); return Objects.equals(valueToMatch, extensionValue); } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index ea5a232cc..dd4ac1dd1 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -879,6 +879,86 @@ class HapiHelperTest extends Specification { result == null } + def "getObr16ExtensionPractitioner should return null when ServiceRequest has no OBR-16 extension"() { + given: + def serviceRequest = new ServiceRequest() + + when: + def result = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) + + then: + result == null + } + + def "getObr16ExtensionPractitioner should return null when OBR-16 extension value is null"() { + given: + def serviceRequest = new ServiceRequest() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = new Extension(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obrExtension.addExtension(obr16Extension) + serviceRequest.addExtension(obrExtension) + + when: + def result = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) + + then: + result == null + } + + def "getObr16ExtensionPractitioner should return null when OBR16 extension value is not a Reference"() { + given: + def serviceRequest = new ServiceRequest() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = new Extension(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obr16Extension.setValue(new StringType("Not a Reference")) + obrExtension.addExtension(obr16Extension) + serviceRequest.addExtension(obrExtension) + + when: + def result = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) + + then: + result == null + } + + def "getObr16ExtensionPractitioner should return null when Reference does not contain Practitioner"() { + given: + def serviceRequest = new ServiceRequest() + def reference = new Reference() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = new Extension(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obr16Extension.setValue(reference) + obrExtension.addExtension(obr16Extension) + serviceRequest.addExtension(obrExtension) + + when: + def result = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) + + then: + result == null + } + + def "getObr16ExtensionPractitioner should return Practitioner when Reference contains Practitioner"() { + given: + def serviceRequest = new ServiceRequest() + def obrExtension = new Extension(HapiHelper.EXTENSION_OBR_URL) + def obr16Extension = new Extension(HapiHelper.EXTENSION_OBR16_DATA_TYPE.toString()) + obrExtension.addExtension(obr16Extension) + serviceRequest.addExtension(obrExtension) + + def practitioner = new Practitioner() + def practitionerRole = new PractitionerRole() + practitionerRole.setPractitioner(new Reference("Practitioner/123").setResource(practitioner) as Reference) + + HapiHelper.setOBR16WithPractitioner(obrExtension, practitionerRole) + + when: + def result = HapiHelper.getObr16ExtensionPractitioner(serviceRequest) + + then: + result == practitioner + } + def "ensureExtensionExists returns extension if it exists"() { given: def serviceRequest = new ServiceRequest() From 14ea77174c0f5c43ecbc9c0589c25cff027e8cc2 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Wed, 18 Dec 2024 10:25:28 -0600 Subject: [PATCH 19/29] Refactor getOrc12ExtensionPractitioner to HapiHelper, add unit tests --- ...OrderProviderToObrOrderProviderTest.groovy | 26 +---- .../external/hapi/HapiHelper.java | 36 ++++++- .../external/hapi/HapiHelperTest.groovy | 95 +++++++++++++++++++ 3 files changed, 129 insertions(+), 28 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index cc983762f..6263274b2 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -192,7 +192,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ } void evaluateOrc12IsNull(Bundle bundle) { - assert getOrc12ExtensionPractitioner(bundle) == null + assert HapiHelper.getOrc12ExtensionPractitioner(bundle) == null } void evaluateOrc12Values( @@ -202,7 +202,7 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ String expectedLastName, String expectedNameTypeCode, String expectedIdentifierTypeCode) { - def practitioner = getOrc12ExtensionPractitioner(bundle) + def practitioner = HapiHelper.getOrc12ExtensionPractitioner(bundle) def xcnExtension = practitioner.getExtensionByUrl(PRACTITIONER_EXTENSION_URL) assert practitioner.identifier[0]?.value == expectedNpi @@ -236,26 +236,4 @@ class CopyOrcOrderProviderToObrOrderProviderTest extends Specification{ def codingSystem = practitioner.identifier[0]?.type?.coding assert codingSystem == null || codingSystem[0]?.code == expectedIdentifierTypeCode } - - Practitioner getOrc12ExtensionPractitioner(Bundle bundle) { - def diagnosticReport = HapiHelper.getDiagnosticReport(bundle) - def serviceRequest = HapiHelper.getServiceRequest(diagnosticReport) - - def orcExtension = serviceRequest.getExtensionByUrl(HapiHelper.EXTENSION_ORC_URL) - def orc12Extension = orcExtension.getExtensionByUrl(HapiHelper.EXTENSION_ORC12_URL) - - if (orc12Extension == null) { - return null - } - - def practitionerReference = (Reference) orc12Extension.getValue() - def practitionerUrl = practitionerReference.getReference() - - for (Bundle.BundleEntryComponent entry : bundle.getEntry()) { - if (Objects.equals(entry.getFullUrl(), practitionerUrl) && entry.getResource() instanceof Practitioner) - return (Practitioner) entry.getResource() - } - - return null - } } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index bef4b214a..a35324c0a 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -551,7 +551,7 @@ public static String getOBR4_1Value(ServiceRequest serviceRequest) { return getCWE1Value(cc.getCoding().get(0)); } - // OBR16 - Ordering Provider + // OBR-16 - Ordering Provider public static void setOBR16WithPractitioner( Extension obrExtension, PractitionerRole practitionerRole) { if (practitionerRole == null || !practitionerRole.getPractitioner().hasReference()) { @@ -578,11 +578,39 @@ public static Practitioner getObr16ExtensionPractitioner(ServiceRequest serviceR return null; } - if (obr16Extension.getValue() instanceof Reference reference) { - if (reference.getResource() instanceof Practitioner practitioner) { + if (obr16Extension.getValue() instanceof Reference reference + && reference.getResource() instanceof Practitioner practitioner) { + return practitioner; + } + return null; + } + + // ORC-12 - Ordering Provider + public static Practitioner getOrc12ExtensionPractitioner(Bundle bundle) { + DiagnosticReport diagnosticReport = getDiagnosticReport(bundle); + if (diagnosticReport == null) return null; + + ServiceRequest serviceRequest = getServiceRequest(diagnosticReport); + if (serviceRequest == null) return null; + + Extension orcExtension = serviceRequest.getExtensionByUrl(EXTENSION_ORC_URL); + if (orcExtension == null) return null; + + Extension orc12Extension = orcExtension.getExtensionByUrl(EXTENSION_ORC12_URL); + if (orc12Extension == null) return null; + + Reference practitionerReference = (Reference) orc12Extension.getValue(); + if (practitionerReference == null) return null; + + String practitionerUrl = practitionerReference.getReference(); + if (practitionerUrl == null) return null; + + for (Bundle.BundleEntryComponent entry : bundle.getEntry()) { + if (Objects.equals(entry.getFullUrl(), practitionerUrl) + && entry.getResource() instanceof Practitioner practitioner) return practitioner; - } } + return null; } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index dd4ac1dd1..9c2cc7cb9 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -2,6 +2,7 @@ package gov.hhs.cdc.trustedintermediary.external.hapi import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.DiagnosticReport import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.MessageHeader @@ -959,6 +960,100 @@ class HapiHelperTest extends Specification { result == practitioner } + // ORC-12 - Ordering Provider + def "getOrc12ExtensionPractitioner should return null when bundle has no DiagnosticReport"() { + given: + def bundle = new Bundle() + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == null + } + + def "getOrc12ExtensionPractitioner should return null when DiagnosticReport has no ServiceRequest"() { + given: + def bundle = new Bundle() + HapiFhirHelper.createDiagnosticReport(bundle) + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == null + } + + def "getOrc12ExtensionPractitioner should return null when ServiceRequest has no ORC extension"() { + given: + def bundle = new Bundle() + def diagnosticReport = HapiFhirHelper.createDiagnosticReport(bundle) + HapiFhirHelper.createBasedOnServiceRequest(diagnosticReport) + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == null + } + + def "getOrc12ExtensionPractitioner should return null when ORC extension has no ORC-12 extension"() { + given: + def bundle = new Bundle() + def diagnosticReport = HapiFhirHelper.createDiagnosticReport(bundle) + def serviceRequest = HapiFhirHelper.createBasedOnServiceRequest(diagnosticReport) + serviceRequest.addExtension(new Extension().setUrl(HapiHelper.EXTENSION_ORC_URL)) + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == null + } + + def "should return null when ORC12 extension has no Practitioner reference"() { + given: + def bundle = new Bundle() + def diagnosticReport = HapiFhirHelper.createDiagnosticReport(bundle) + def serviceRequest = HapiFhirHelper.createBasedOnServiceRequest(diagnosticReport) + + def orcExtension = new Extension().setUrl(HapiHelper.EXTENSION_ORC_URL) + def orc12Extension = new Extension().setUrl(HapiHelper.EXTENSION_ORC12_URL) + orcExtension.addExtension(orc12Extension) + serviceRequest.addExtension(orcExtension) + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == null + } + + def "should return Practitioner when all conditions are met"() { + given: + def bundle = new Bundle() + def diagnosticReport = HapiFhirHelper.createDiagnosticReport(bundle) + def serviceRequest = HapiFhirHelper.createBasedOnServiceRequest(diagnosticReport) + + def orcExtension = new Extension().setUrl(HapiHelper.EXTENSION_ORC_URL) + def orc12Extension = new Extension().setUrl(HapiHelper.EXTENSION_ORC12_URL) + + def practitioner = new Practitioner() + def practitionerId = "Practitioner/123" + def practitionerReference = new Reference().setReference(practitionerId) + + orc12Extension.setValue(practitionerReference) + orcExtension.addExtension(orc12Extension) + serviceRequest.addExtension(orcExtension) + bundle.addEntry(new Bundle.BundleEntryComponent().setFullUrl(practitionerId).setResource(practitioner)) + + when: + def result = HapiHelper.getOrc12ExtensionPractitioner(bundle) + + then: + result == practitioner + } + def "ensureExtensionExists returns extension if it exists"() { given: def serviceRequest = new ServiceRequest() From b23b0aeb6cf6332523ce71d423a417b1227a2e72 Mon Sep 17 00:00:00 2001 From: Joel Biskie Date: Wed, 18 Dec 2024 14:39:06 -0600 Subject: [PATCH 20/29] Fix linting --- .../custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy | 3 --- .../trustedintermediary/external/hapi/HapiHelperTest.groovy | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy index 6263274b2..a7db7112d 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/CopyOrcOrderProviderToObrOrderProviderTest.groovy @@ -7,9 +7,6 @@ import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.DiagnosticReport -import org.hl7.fhir.r4.model.Extension -import org.hl7.fhir.r4.model.Practitioner -import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.ServiceRequest import spock.lang.Specification diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 9c2cc7cb9..54283fc92 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -561,11 +561,11 @@ class HapiHelperTest extends Specification { HapiFhirHelper.createPIDPatient(bundle) HapiFhirHelper.setPID3Identifier(bundle, patientIdentifier) - patientIdentifier.addExtension().setUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL); + patientIdentifier.addExtension().setUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) patientIdentifier .getExtensionByUrl(HapiHelper.EXTENSION_CX_IDENTIFIER_URL) .addExtension() - .setUrl(HapiHelper.EXTENSION_XON10_URL); + .setUrl(HapiHelper.EXTENSION_XON10_URL) when: HapiHelper.removePID3_5Value(bundle) From 5d991fec1e62153e3294daec5e145b6093587ea5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 05:32:00 +0000 Subject: [PATCH 21/29] Update dependency io.github.cdimascio:dotenv-java to v3.1.0 (#1666) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- shared/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/build.gradle b/shared/build.gradle index 3d90afc7b..2f5caad8c 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -58,7 +58,7 @@ dependencies { testFixturesImplementation 'nl.jqno.equalsverifier:equalsverifier:3.17.5' // dotenv-java - implementation 'io.github.cdimascio:dotenv-java:3.0.2' + implementation 'io.github.cdimascio:dotenv-java:3.1.0' // postgres implementation 'org.postgresql:postgresql:42.7.4' From b1e4c4df77ff3d3d654b5d975a90ff2bd31dcfe7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:34:45 +0000 Subject: [PATCH 22/29] Update dependency ch.qos.logback:logback-classic to v1.5.14 (#1668) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- shared/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/build.gradle b/shared/build.gradle index 2f5caad8c..a2f7a3327 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -18,7 +18,7 @@ dependencies { // logging implementation 'org.slf4j:slf4j-api:2.0.16' - implementation 'ch.qos.logback:logback-classic:1.5.13' + implementation 'ch.qos.logback:logback-classic:1.5.14' implementation 'net.logstash.logback:logstash-logback-encoder:8.0' //jackson From 6d78157bf08a7dc83a92bab3e17503de525c78ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:16:07 +0000 Subject: [PATCH 23/29] Update patch dependencies to v7.6.1 (#1669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- e2e/build.gradle | 8 ++++---- shared/build.gradle | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/e2e/build.gradle b/e2e/build.gradle index bed2506f8..238087eae 100644 --- a/e2e/build.gradle +++ b/e2e/build.gradle @@ -19,10 +19,10 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' //fhir - implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-caching-caffeine:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:7.6.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:7.6.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.6.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-caching-caffeine:7.6.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:7.6.1' implementation 'org.fhir:ucum:1.0.9' testImplementation 'org.apache.groovy:groovy:4.0.24' diff --git a/shared/build.gradle b/shared/build.gradle index a2f7a3327..be825e88f 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -28,10 +28,10 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' // hapi fhir - api 'ca.uhn.hapi.fhir:hapi-fhir-base:7.6.0' - api 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-caching-caffeine:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:7.6.0' + api 'ca.uhn.hapi.fhir:hapi-fhir-base:7.6.1' + api 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:7.6.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-caching-caffeine:7.6.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:7.6.1' api 'org.fhir:ucum:1.0.9' // hapi hl7 From 0ba5e27e54261a274080a89c3aecb4cf4ca29930 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:05:43 +0000 Subject: [PATCH 24/29] Update dependency io.javalin:javalin to v6.4.0 (#1670) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 17554eeb1..29853035f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation project(':etor') testImplementation testFixtures(project(':shared')) - implementation 'io.javalin:javalin:6.3.0' + implementation 'io.javalin:javalin:6.4.0' testImplementation 'org.apache.groovy:groovy:4.0.24' testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' From 179a448bc5090366674528088227d69f78cc70af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:30:24 +0000 Subject: [PATCH 25/29] Update dependency ch.qos.logback:logback-classic to v1.5.15 (#1671) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- shared/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/build.gradle b/shared/build.gradle index be825e88f..cdaa01e52 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -18,7 +18,7 @@ dependencies { // logging implementation 'org.slf4j:slf4j-api:2.0.16' - implementation 'ch.qos.logback:logback-classic:1.5.14' + implementation 'ch.qos.logback:logback-classic:1.5.15' implementation 'net.logstash.logback:logstash-logback-encoder:8.0' //jackson From b6eeec585cf1f6990135acc7890b2304e541a776 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:41:00 +0000 Subject: [PATCH 26/29] Update dependency gradle to v8.12 (#1672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c820..cea7a793a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6..f3b75f3b0 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From 6ff05fc4ee4c218e309c9d499a132f945ed0fa15 Mon Sep 17 00:00:00 2001 From: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:54:03 -0800 Subject: [PATCH 27/29] Fix for ZAP scan warning: `Insufficient Site Isolation Against Spectre Vulnerability` (#1675) * Trying fix for zap scan warning * Removing fix to confirm alert * Adding one fix back to check for minimum required * Fix is too restrictive. Trying another way * Trial and error * Trial and error * Trial and error * Trial and error * Trial and error * Trial and error --- .../hhs/cdc/trustedintermediary/external/javalin/App.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java b/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java index 2d707ba0f..10b63d090 100644 --- a/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java +++ b/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java @@ -48,7 +48,12 @@ public static void main(String[] args) { // apply this security header to all responses, but allow it to be overwritten by a specific // endpoint by using `before` if needed - app.before(ctx -> ctx.header("X-Content-Type-Options", "nosniff")); + app.before( + ctx -> { + ctx.header("X-Content-Type-Options", "nosniff"); + // Fix for https://www.zaproxy.org/docs/alerts/90004 + ctx.header("Cross-Origin-Resource-Policy", "cross-origin"); + }); try { app.get(HEALTH_API_ENDPOINT, ctx -> ctx.result("Operational")); From 1a52e1b8b389df54fb7c287df91eda191fe306d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:39:43 +0000 Subject: [PATCH 28/29] Update dependency nl.jqno.equalsverifier:equalsverifier to v3.18 (#1676) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle | 2 +- etor/build.gradle | 2 +- shared/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 29853035f..87b7144ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ dependencies { testImplementation 'org.apache.groovy:groovy:4.0.24' testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'com.openpojo:openpojo:0.9.1' - testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.17.5' + testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.18' } jacocoTestCoverageVerification { diff --git a/etor/build.gradle b/etor/build.gradle index b69534ae7..36f2e2b9e 100644 --- a/etor/build.gradle +++ b/etor/build.gradle @@ -19,7 +19,7 @@ dependencies { testImplementation 'org.apache.groovy:groovy:4.0.24' testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'com.openpojo:openpojo:0.9.1' - testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.17.5' + testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.18' } jacocoTestCoverageVerification { diff --git a/shared/build.gradle b/shared/build.gradle index cdaa01e52..813149042 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -55,7 +55,7 @@ dependencies { testFixturesImplementation 'org.apache.groovy:groovy:4.0.24' testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testFixturesImplementation 'com.openpojo:openpojo:0.9.1' - testFixturesImplementation 'nl.jqno.equalsverifier:equalsverifier:3.17.5' + testFixturesImplementation 'nl.jqno.equalsverifier:equalsverifier:3.18' // dotenv-java implementation 'io.github.cdimascio:dotenv-java:3.1.0' From 0288de95587d9d70a6378d1ef11fe242b4372704 Mon Sep 17 00:00:00 2001 From: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:26:55 -0800 Subject: [PATCH 29/29] Refactor rs-e2e project to remove Hapi HL7 library (#1654) * WIP: refactored to remove hapi hl7 library Co-authored-by: luis-pabon-tf * Moved files to more appropiate location * Update HL7MessageTest.groovy Remove library usage on test class, one test not finished yet. * Accounting for edge cases * Made class with static methods static * Improved delimiter handling and added test assertions * Handling additional edge case * Update HL7MessageTest.groovy Corrected structure on test object * Improved encoding character handling to use defaults and move to HL7Parser * Update HapiHL7ExpressionEvaluatorTest.groovy Replacing Message with HL7Message, some incompatible methods remain * Extracted parsing logic to HL7Parser * Added method to return string of message and fixed a couple of tests * Moved flies from /external/hapi folder to /hl7 folder * Renamed files to remove Hapi prefix * Renamed files to remove Hapi prefix * Replaced magic strings * Update HL7Message.java Add method for getting segment counts * Update HL7ExpressionEvaluator.java Adding replacement method for evaluation collection counts * Updating tests and some cleanup * Update HL7ExpressionEvaluator.java Adding some deprecation flags and replacement methods * Update HL7ExpressionEvaluatorTest.groovy Updated test data and a mock * Refactored for HL7ExpressionEvaluator to use the new parser and remove hapi dependency Co-authored-by: luis-pabon-tf * Update HL7ExpressionEvaluatorTest.groovy Most tests are passing, and HAPI Message is gone now. * Fixed test + cleanup * Fixed a couple of issues in HL7Message and refactored to extract segment into a HL7Segment record Co-authored-by: luis-pabon-tf * Fixed HL7MessageTest tests * Added HL7MessageException, improved error handling in HL7Message, refined regex pattern and fixed test * Update HL7FileMatcherTest.groovy Added more code coverage * Added test cobverage for HL7Message and fixed parsing issue * Add more HL7Parser coverage * Added test coverage for HL7ParserTest * Removed unused HL7Segment.getIndex * Completed HL7Parser coverage * Removed gradle dependency to the hapi library and removed last few mentions of Hapi * Added test coverage and changed exception type * Fixed a couple of issues brought up by sonar * Replacing IllegalArgumentException with HL7ParserException custom exception * Changed parser behaviour to return an empty string instead of null when value not found. Throwing an exception if hl7 path is not allowed Co-authored-by: luis-pabon-tf * Added a not to the readme about returning an empty string when value not found Co-authored-by: luis-pabon-tf * Added javadocs * Refactored for better separation of concerns. Created HL7Path and HL7Encoding for this purpose * Fixed tests, added test coverage and some more refactoring * Fixed a few more tests. A couple more to go * Added javadocs * Updated edge case handling and fixed test * Fixed remaining test * Removed unnecessary logic * Override equals method in HL7Path to address issue raised by SonarCloud * Need to override hashCode and toString as well * Added test coverage for new methods * Moved logic in HL7Parser.parseMessageFieldValue to HL7Message.getValue to avoid circular dependency * Moved logic in HL7Parser.parsePath to HL7Path.parse to avoid circular dependency * Removed HL7Parser and moved message parsing logic to HL7Message to avoid circular dependencies * Fixed equals overload logic * Update HL7MessageException.java Removed unused constructor * Update HL7ExpressionEvaluatorTest.groovy Added a little more coverage --------- Co-authored-by: luis-pabon-tf Co-authored-by: Luis Pabon --- rs-e2e/README.md | 1 + rs-e2e/build.gradle | 4 - .../rse2e/FileFetcher.java | 1 + .../azure}/AzureBlobFileFetcher.java | 4 +- .../{ => external/azure}/AzureBlobHelper.java | 2 +- .../azure}/AzureBlobOrganizer.java | 2 +- .../hapi/HapiHL7ExpressionEvaluator.java | 237 ------------------ .../external/hapi/HapiHL7FileMatcher.java | 92 ------- .../hapi/HapiHL7FileMatcherException.java | 16 -- .../rse2e/external/hapi/HapiHL7Message.java | 34 --- .../localfile}/LocalFileFetcher.java | 7 +- .../rse2e/hl7/HL7Encoding.java | 78 ++++++ .../rse2e/hl7/HL7ExpressionEvaluator.java | 158 ++++++++++++ .../rse2e/hl7/HL7FileMatcher.java | 75 ++++++ .../rse2e/hl7/HL7FileMatcherException.java | 16 ++ .../rse2e/{ => hl7}/HL7FileStream.java | 2 +- .../rse2e/hl7/HL7Message.java | 141 +++++++++++ .../rse2e/hl7/HL7MessageException.java | 12 + .../rse2e/hl7/HL7ParserException.java | 16 ++ .../rse2e/hl7/HL7Path.java | 43 ++++ .../rse2e/hl7/HL7Segment.java | 6 + .../rse2e/AutomatedTest.groovy | 15 +- .../azure}/AzureBlobHelperTest.groovy | 3 +- .../external/hapi/HapiHL7MessageTest.groovy | 35 --- .../rse2e/hl7/HL7EncodingTest.groovy | 55 ++++ .../HL7ExpressionEvaluatorTest.groovy} | 217 +++++----------- .../HL7FileMatcherExceptionTest.groovy} | 9 +- .../HL7FileMatcherTest.groovy} | 61 +++-- .../rse2e/hl7/HL7MessageTest.groovy | 155 ++++++++++++ .../rse2e/hl7/HL7PathTest.groovy | 61 +++++ .../ruleengine/AssertionRuleEngineTest.groovy | 14 +- .../rse2e/ruleengine/RuleLoaderTest.groovy | 4 +- 32 files changed, 951 insertions(+), 625 deletions(-) rename rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/{ => external/azure}/AzureBlobFileFetcher.java (93%) rename rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/{ => external/azure}/AzureBlobHelper.java (93%) rename rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/{ => external/azure}/AzureBlobOrganizer.java (97%) delete mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java delete mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java delete mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherException.java delete mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java rename rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/{ => external/localfile}/LocalFileFetcher.java (89%) create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Encoding.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluator.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcher.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherException.java rename rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/{ => hl7}/HL7FileStream.java (81%) create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Message.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageException.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ParserException.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Path.java create mode 100644 rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Segment.java rename rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/{ => external/azure}/AzureBlobHelperTest.groovy (93%) delete mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7EncodingTest.groovy rename rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/{external/hapi/HapiHL7ExpressionEvaluatorTest.groovy => hl7/HL7ExpressionEvaluatorTest.groovy} (55%) rename rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/{external/hapi/HapiHL7FileMatcherExceptionTest.groovy => hl7/HL7FileMatcherExceptionTest.groovy} (65%) rename rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/{external/hapi/HapiHL7FileMatcherTest.groovy => hl7/HL7FileMatcherTest.groovy} (72%) create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageTest.groovy create mode 100644 rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7PathTest.groovy diff --git a/rs-e2e/README.md b/rs-e2e/README.md index 0b3f36945..77becbc2e 100644 --- a/rs-e2e/README.md +++ b/rs-e2e/README.md @@ -10,6 +10,7 @@ Information on how to set up the sample files evaluated by the tests can be foun - The output files generated by the framework are stored in an Azure blob storage container. Every time the tests are run, a cleanup task moves the files to a folder with the year/month/day format for better organization. The files are retained in the container for 90 days before being deleted - The code that organizes the files is using EST time zone. This means that if you are in PST, you may run into an issue if you submit a run before 9 PM PST and then run the tests after 9pm. You'd need to make sure to run both tasks before or after 9pm so the files are where they are expected to be +- The HL7 parser and expression evaluator returns an empty string when the value is not found. It will only throw an exception if the path is not a valid HL7 notation ## Running the tests diff --git a/rs-e2e/build.gradle b/rs-e2e/build.gradle index bff4eafe8..d5c248b3a 100644 --- a/rs-e2e/build.gradle +++ b/rs-e2e/build.gradle @@ -20,10 +20,6 @@ dependencies { // azure implementation 'com.azure:azure-storage-blob:12.29.0' - // hapi hl7 - implementation 'ca.uhn.hapi:hapi-base:2.5.1' - implementation 'ca.uhn.hapi:hapi-structures-v251:2.5.1' - testImplementation 'org.apache.groovy:groovy:4.0.24' testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' } diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java index dcd998148..0e242f6a2 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/FileFetcher.java @@ -1,5 +1,6 @@ package gov.hhs.cdc.trustedintermediary.rse2e; +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7FileStream; import java.util.List; /** diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobFileFetcher.java similarity index 93% rename from rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java rename to rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobFileFetcher.java index 4d9a3a478..e22b6d0fd 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobFileFetcher.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobFileFetcher.java @@ -1,10 +1,12 @@ -package gov.hhs.cdc.trustedintermediary.rse2e; +package gov.hhs.cdc.trustedintermediary.rse2e.external.azure; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobContainerClientBuilder; import com.azure.storage.blob.models.BlobItem; import com.azure.storage.blob.models.ListBlobsOptions; +import gov.hhs.cdc.trustedintermediary.rse2e.FileFetcher; +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7FileStream; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZoneOffset; diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelper.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelper.java similarity index 93% rename from rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelper.java rename to rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelper.java index d6548229b..5423e1bdd 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelper.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelper.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.rse2e; +package gov.hhs.cdc.trustedintermediary.rse2e.external.azure; import java.time.LocalDate; diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobOrganizer.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobOrganizer.java similarity index 97% rename from rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobOrganizer.java rename to rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobOrganizer.java index 4f61997e0..1c1cc3b99 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobOrganizer.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobOrganizer.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.rse2e; +package gov.hhs.cdc.trustedintermediary.rse2e.external.azure; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java deleted file mode 100644 index 563a76823..000000000 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluator.java +++ /dev/null @@ -1,237 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; - -import ca.uhn.hl7v2.HL7Exception; -import ca.uhn.hl7v2.model.Message; -import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; -import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; -import java.util.Arrays; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * The HapiHL7ExpressionEvaluator class is responsible for evaluating expressions on HL7 messages. - * The expressions can be used to compare fields, count segments, and check for membership. The - * expressions are in the form of: `field = value`, `field != value`, `field in (value1, value2)`, - * `field.count() = value`, etc. The field can be a literal value (e.g. 'EPIC') or a field reference - * (e.g. MSH-5.1, input.MSH-5.1). - */ -public class HapiHL7ExpressionEvaluator implements HealthDataExpressionEvaluator { - - private static final HapiHL7ExpressionEvaluator INSTANCE = new HapiHL7ExpressionEvaluator(); - - private static final String NEWLINE_REGEX = "\\r?\\n|\\r"; - private static final Pattern OPERATION_PATTERN = - Pattern.compile("^(\\S+)\\s*(=|!=|in)\\s*(.+)$"); - private static final Pattern HL7_COUNT_PATTERN = Pattern.compile("(\\S+)\\.count\\(\\)"); - private static final Pattern LITERAL_VALUE_PATTERN = Pattern.compile("'(.*)'"); - private static final Pattern LITERAL_VALUE_COLLECTION_PATTERN = - Pattern.compile("\\(([^)]+)\\)"); - private static final Pattern MESSAGE_SOURCE_PATTERN = - Pattern.compile("(input|output)?\\.?(\\S+)"); - private static final Pattern HL7_FIELD_NAME_PATTERN = Pattern.compile("(\\w+)(?:-(\\S+))?"); - - private HapiHL7ExpressionEvaluator() {} - - public static HapiHL7ExpressionEvaluator getInstance() { - return INSTANCE; - } - - @Override - public final boolean evaluateExpression(String expression, HealthData... data) { - if (data.length > 2) { - throw new IllegalArgumentException( - "Expected two messages, but received: " + data.length); - } - - Matcher matcher = OPERATION_PATTERN.matcher(expression); - - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid statement format."); - } - - String leftOperand = matcher.group(1); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', OBR.count() - String operator = matcher.group(2); // `=`, `!=`, or `in` - String rightOperand = - matcher.group(3); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', ('EPIC', 'CERNER'), 2 - - Message outputMessage = (Message) data[0].getUnderlyingData(); - Message inputMessage = (data.length > 1) ? (Message) data[1].getUnderlyingData() : null; - - // matches a count operation (e.g. OBR.count()) - Matcher hl7CountMatcher = HL7_COUNT_PATTERN.matcher(leftOperand); - if (hl7CountMatcher.matches()) { - return evaluateCollectionCount( - outputMessage, hl7CountMatcher.group(1), rightOperand, operator); - } - - // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, - // input.MSH-5.1) - String leftValue = getLiteralOrFieldValue(outputMessage, inputMessage, leftOperand); - - // matches membership operator (e.g. MSH-5.1 in ('EPIC', 'CERNER')) - if (operator.equals("in")) { - return evaluateMembership(leftValue, rightOperand); - } - - // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, - // input.MSH-5.1) - String rightValue = getLiteralOrFieldValue(outputMessage, inputMessage, rightOperand); - - // matches equality operators (e.g. MSH-5.1 = 'EPIC', MSH-5.1 != 'EPIC') - return evaluateEquality(leftValue, rightValue, operator); - } - - protected > boolean evaluateEquality( - T leftValue, T rightValue, String operator) { - if (operator.equals("=")) { - return leftValue.equals(rightValue); - } else if (operator.equals("!=")) { - return !leftValue.equals(rightValue); - } - throw new IllegalArgumentException("Unknown operator: " + operator); - } - - protected boolean evaluateMembership(String leftValue, String rightOperand) { - Matcher literalValueCollectionMatcher = - LITERAL_VALUE_COLLECTION_PATTERN.matcher(rightOperand); - if (!literalValueCollectionMatcher.matches()) { - throw new IllegalArgumentException("Invalid collection format: " + rightOperand); - } - String arrayString = literalValueCollectionMatcher.group(1); - Set values = - Arrays.stream(arrayString.split(",")) - .map(s -> s.trim().replace("'", "")) - .collect(Collectors.toSet()); - return values.contains(leftValue); - } - - protected boolean evaluateCollectionCount( - Message message, String segmentName, String rightOperand, String operator) { - try { - int count = countSegments(message.encode(), segmentName); - int rightValue = Integer.parseInt(rightOperand); - return evaluateEquality(count, rightValue, operator); - } catch (HL7Exception | NumberFormatException e) { - throw new IllegalArgumentException( - "Error evaluating collection count. Segment: " - + segmentName - + ", count: " - + rightOperand, - e); - } - } - - protected String getLiteralOrFieldValue( - Message outputMessage, Message inputMessage, String operand) { - Matcher literalValueMatcher = LITERAL_VALUE_PATTERN.matcher(operand); - if (literalValueMatcher.matches()) { - return literalValueMatcher.group(1); - } - return getFieldValue(outputMessage, inputMessage, operand); - } - - protected String getFieldValue(Message outputMessage, Message inputMessage, String fieldName) { - Matcher messageSourceMatcher = MESSAGE_SOURCE_PATTERN.matcher(fieldName); - if (!messageSourceMatcher.matches()) { - throw new IllegalArgumentException("Invalid field name format: " + fieldName); - } - - String fileSource = messageSourceMatcher.group(1); - String fieldNameWithoutFileSource = messageSourceMatcher.group(2); - Message message = getMessageBySource(fileSource, inputMessage, outputMessage); - - try { - String messageString = message.encode(); - char fieldSeparator = message.getFieldSeparatorValue(); - String encodingCharacters = message.getEncodingCharactersValue(); - return getSegmentFieldValue( - messageString, fieldNameWithoutFileSource, fieldSeparator, encodingCharacters); - } catch (HL7Exception | NumberFormatException e) { - throw new IllegalArgumentException( - "Failed to extract field value for: " + fieldName, e); - } - } - - // We decided to implement our own simple HL7 parser as the Hapi library was not adequate for - // our needs. - protected static String getSegmentFieldValue( - String hl7Message, String fieldName, char fieldSeparator, String encodingCharacters) { - Matcher hl7FieldNameMatcher = HL7_FIELD_NAME_PATTERN.matcher(fieldName); - if (!hl7FieldNameMatcher.matches()) { - throw new IllegalArgumentException("Invalid HL7 field format: " + fieldName); - } - - String segmentName = hl7FieldNameMatcher.group(1); - String segmentFieldIndex = hl7FieldNameMatcher.group(2); - - String[] lines = hl7Message.split(NEWLINE_REGEX); - for (String line : lines) { - if (!line.startsWith(segmentName)) { - continue; - } - - if (segmentFieldIndex == null) { - return line; - } - - String[] fields = line.split(Pattern.quote(String.valueOf(fieldSeparator))); - String[] indexParts = segmentFieldIndex.split("\\."); - - try { - int fieldPos = Integer.parseInt(indexParts[0]); - - if (segmentName.equals("MSH")) { - fieldPos--; - } - - if (fieldPos < 0 || fieldPos >= fields.length) { - throw new IllegalArgumentException( - "Invalid field index (out of bounds): " + segmentFieldIndex); - } - - String field = fields[fieldPos]; - - if (indexParts.length == 1 || field.isEmpty()) { - return field; - } - - int subFieldEncodingCharactersIndex = indexParts.length - 2; - if (subFieldEncodingCharactersIndex >= encodingCharacters.length()) { - throw new IllegalArgumentException( - "Invalid subfield index (out of bounds): " + segmentFieldIndex); - } - char subfieldSeparator = encodingCharacters.charAt(subFieldEncodingCharactersIndex); - String[] subfields = field.split(Pattern.quote(String.valueOf(subfieldSeparator))); - int subFieldPos = Integer.parseInt(indexParts[1]) - 1; - return subFieldPos >= 0 && subFieldPos < subfields.length - ? subfields[subFieldPos] - : ""; - } catch (NumberFormatException e) { - throw new IllegalArgumentException( - "Invalid field index formatting: " + segmentFieldIndex, e); - } - } - - return null; - } - - protected static int countSegments(String hl7Message, String segmentName) { - return (int) - Arrays.stream(hl7Message.split(NEWLINE_REGEX)) - .filter(line -> line.startsWith(segmentName)) - .count(); - } - - protected Message getMessageBySource( - String source, Message inputMessage, Message outputMessage) { - if ("input".equals(source)) { - if (inputMessage == null) { - throw new IllegalArgumentException("Input message is null for: " + source); - } - return inputMessage; - } - return outputMessage; - } -} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java deleted file mode 100644 index ca9ca419f..000000000 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcher.java +++ /dev/null @@ -1,92 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; - -import ca.uhn.hl7v2.DefaultHapiContext; -import ca.uhn.hl7v2.HL7Exception; -import ca.uhn.hl7v2.HapiContext; -import ca.uhn.hl7v2.model.Message; -import ca.uhn.hl7v2.parser.Parser; -import com.google.common.collect.Sets; -import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * The HapiHL7FileMatcher class is responsible for matching input and output HL7 files based on the - * control ID (MSH-10). - */ -public class HapiHL7FileMatcher { - - private static final HapiHL7FileMatcher INSTANCE = new HapiHL7FileMatcher(); - - private HapiHL7FileMatcher() {} - - public static HapiHL7FileMatcher getInstance() { - return INSTANCE; - } - - public Map matchFiles( - List outputFiles, List inputFiles) - throws HapiHL7FileMatcherException { - // We pair up output and input files based on the control ID, which is in MSH-10 - // Any files (either input or output) that don't have a match are logged - Map inputMap = parseAndMapMessageByControlId(inputFiles); - Map outputMap = parseAndMapMessageByControlId(outputFiles); - - Set inputKeys = inputMap.keySet(); - Set outputKeys = outputMap.keySet(); - Set unmatchedKeys = new HashSet<>(); - unmatchedKeys.addAll(Sets.difference(inputKeys, outputKeys)); - unmatchedKeys.addAll(Sets.difference(outputKeys, inputKeys)); - - if (!unmatchedKeys.isEmpty()) { - throw new HapiHL7FileMatcherException( - "Found no match for messages with the following MSH-10 values: " - + unmatchedKeys); - } - - return inputKeys.stream().collect(Collectors.toMap(inputMap::get, outputMap::get)); - } - - public Map parseAndMapMessageByControlId(List files) - throws HapiHL7FileMatcherException { - - Map messageMap = new HashMap<>(); - - try (HapiContext context = new DefaultHapiContext()) { - Parser parser = context.getPipeParser(); - - for (HL7FileStream hl7FileStream : files) { - String fileName = hl7FileStream.fileName(); - try (InputStream inputStream = hl7FileStream.inputStream()) { - String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - Message message = parser.parse(content); - HapiHL7Message hapiHL7Message = new HapiHL7Message(message); - String msh10 = hapiHL7Message.getIdentifier(); - if (msh10 == null || msh10.isEmpty()) { - throw new HapiHL7FileMatcherException( - String.format("MSH-10 is empty for file: %s", fileName)); - } - messageMap.put(msh10, hapiHL7Message); - } catch (HL7Exception e) { - throw new HapiHL7FileMatcherException( - String.format("Failed to parse HL7 message from file: %s", fileName), - e); - } catch (IOException e) { - throw new HapiHL7FileMatcherException( - String.format("Failed to read file: %s", fileName), e); - } - } - } catch (IOException e) { - throw new HapiHL7FileMatcherException("Failed to close input stream", e); - } - - return messageMap; - } -} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherException.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherException.java deleted file mode 100644 index fc3e0956d..000000000 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherException.java +++ /dev/null @@ -1,16 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; - -/** - * The HapiHL7FileMatcherException class is responsible for handling exceptions that occur in the - * HapiHL7FileMatcher class. - */ -public class HapiHL7FileMatcherException extends Exception { - - public HapiHL7FileMatcherException(String message, Throwable cause) { - super(message, cause); - } - - public HapiHL7FileMatcherException(String message) { - super(message); - } -} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java deleted file mode 100644 index 2ff732add..000000000 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7Message.java +++ /dev/null @@ -1,34 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi; - -import ca.uhn.hl7v2.HL7Exception; -import ca.uhn.hl7v2.model.Message; -import ca.uhn.hl7v2.model.v251.segment.MSH; -import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; - -/** - * Represents a HAPI HL7 message that implements the HealthData interface. This class provides a - * wrapper around the HAPI Message object. - */ -public class HapiHL7Message implements HealthData { - - protected final Message underlyingData; - - public HapiHL7Message(Message innerResource) { - this.underlyingData = innerResource; - } - - @Override - public Message getUnderlyingData() { - return underlyingData; - } - - @Override - public String getIdentifier() { - try { - MSH mshSegment = (MSH) underlyingData.get("MSH"); - return mshSegment.getMessageControlID().getValue(); - } catch (HL7Exception e) { - return null; - } - } -} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/localfile/LocalFileFetcher.java similarity index 89% rename from rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java rename to rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/localfile/LocalFileFetcher.java index 3172c5e36..308547586 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/LocalFileFetcher.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/external/localfile/LocalFileFetcher.java @@ -1,12 +1,13 @@ -package gov.hhs.cdc.trustedintermediary.rse2e; +package gov.hhs.cdc.trustedintermediary.rse2e.external.localfile; +import gov.hhs.cdc.trustedintermediary.rse2e.FileFetcher; +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7FileStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -43,7 +44,7 @@ public List fetchFiles() { throw new RuntimeException("Error opening file: " + p, e); } }) - .collect(Collectors.toList()); + .toList(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Encoding.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Encoding.java new file mode 100644 index 000000000..1ab35ae7a --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Encoding.java @@ -0,0 +1,78 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import java.util.Map; + +/** The HL7Encoding class represents the encoding characters used in an HL7 message. */ +public record HL7Encoding(Map characters) { + public static final String DEFAULT_SEGMENT_DELIMITER = "\n"; + public static final char DEFAULT_FIELD_DELIMITER = '|'; + public static final char DEFAULT_COMPONENT_DELIMITER = '^'; + public static final char DEFAULT_REPETITION_DELIMITER = '~'; + public static final char DEFAULT_ESCAPE_CHARACTER = '\\'; + public static final char DEFAULT_SUBCOMPONENT_DELIMITER = '&'; + + public static final String FIELD_DELIMITER_NAME = "field"; + public static final String COMPONENT_DELIMITER_NAME = "component"; + public static final String REPETITION_DELIMITER_NAME = "repetition"; + public static final String ESCAPE_CHARACTER_NAME = "escape"; + public static final String SUBCOMPONENT_DELIMITER_NAME = "subcomponent"; + + private static final Map DEFAULT_CHARACTERS = + Map.of( + FIELD_DELIMITER_NAME, DEFAULT_FIELD_DELIMITER, + COMPONENT_DELIMITER_NAME, DEFAULT_COMPONENT_DELIMITER, + REPETITION_DELIMITER_NAME, DEFAULT_REPETITION_DELIMITER, + ESCAPE_CHARACTER_NAME, DEFAULT_ESCAPE_CHARACTER, + SUBCOMPONENT_DELIMITER_NAME, DEFAULT_SUBCOMPONENT_DELIMITER); + + public static HL7Encoding defaultEncoding() { + return new HL7Encoding(DEFAULT_CHARACTERS); + } + + public static HL7Encoding fromEncodingField(String encodingField) { + if (encodingField == null || encodingField.isEmpty()) { + return defaultEncoding(); + } + + return new HL7Encoding( + Map.of( + FIELD_DELIMITER_NAME, DEFAULT_FIELD_DELIMITER, + COMPONENT_DELIMITER_NAME, encodingField.charAt(0), + REPETITION_DELIMITER_NAME, encodingField.charAt(1), + ESCAPE_CHARACTER_NAME, encodingField.charAt(2), + SUBCOMPONENT_DELIMITER_NAME, encodingField.charAt(3))); + } + + public char getCharacter(String type) { + return characters.get(type); + } + + public char getEscapeCharacter() { + return characters.get(ESCAPE_CHARACTER_NAME); + } + + public char getFieldDelimiter() { + return characters.get(FIELD_DELIMITER_NAME); + } + + public char getComponentDelimiter() { + return characters.get(COMPONENT_DELIMITER_NAME); + } + + public char getRepetitionDelimiter() { + return characters.get(REPETITION_DELIMITER_NAME); + } + + public char getSubcomponentDelimiter() { + return characters.get(SUBCOMPONENT_DELIMITER_NAME); + } + + public char[] getOrderedDelimiters() { + return new char[] { + getCharacter(FIELD_DELIMITER_NAME), + getCharacter(COMPONENT_DELIMITER_NAME), + getCharacter(REPETITION_DELIMITER_NAME), + getCharacter(SUBCOMPONENT_DELIMITER_NAME) + }; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluator.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluator.java new file mode 100644 index 000000000..77e89a304 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluator.java @@ -0,0 +1,158 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator; +import java.util.Arrays; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * The HL7ExpressionEvaluator class is responsible for evaluating expressions on HL7 messages. The + * expressions can be used to compare fields, count segments, and check for membership. The + * expressions are in the form of: `field = value`, `field != value`, `field in (value1, value2)`, + * `field.count() = value`, etc. The field can be a literal value (e.g. 'EPIC') or a field reference + * (e.g. MSH-5.1, input.MSH-5.1). + */ +public class HL7ExpressionEvaluator implements HealthDataExpressionEvaluator { + + private static final HL7ExpressionEvaluator INSTANCE = new HL7ExpressionEvaluator(); + + private static final Pattern OPERATION_PATTERN = + Pattern.compile("^(\\S+)\\s*(=|!=|in)\\s*(.+)$"); + private static final Pattern HL7_COUNT_PATTERN = Pattern.compile("(\\S+)\\.count\\(\\)"); + private static final Pattern LITERAL_VALUE_PATTERN = Pattern.compile("'(.*)'"); + private static final Pattern LITERAL_VALUE_COLLECTION_PATTERN = + Pattern.compile("\\(([^)]+)\\)"); + private static final Pattern MESSAGE_SOURCE_PATTERN = + Pattern.compile("(input|output)?\\.?(\\S+)"); + + private HL7ExpressionEvaluator() {} + + public static HL7ExpressionEvaluator getInstance() { + return INSTANCE; + } + + @Override + public final boolean evaluateExpression(String expression, HealthData... data) { + if (data.length > 2) { + throw new HL7ParserException("Expected two messages, but received: " + data.length); + } + + Matcher matcher = OPERATION_PATTERN.matcher(expression); + + if (!matcher.matches()) { + throw new HL7ParserException("Invalid statement format."); + } + + String leftOperand = matcher.group(1); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', OBR.count() + String operator = matcher.group(2); // `=`, `!=`, or `in` + String rightOperand = + matcher.group(3); // e.g. MSH-5.1, input.MSH-5.1, 'EPIC', ('EPIC', 'CERNER'), 2 + + HL7Message outputMessage = (HL7Message) data[0]; + HL7Message inputMessage = (data.length > 1) ? (HL7Message) data[1] : null; + + // matches a count operation (e.g. OBR.count()) + Matcher hl7CountMatcher = HL7_COUNT_PATTERN.matcher(leftOperand); + if (hl7CountMatcher.matches()) { + return evaluateCollectionCount( + outputMessage, hl7CountMatcher.group(1), rightOperand, operator); + } + + // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, + // input.MSH-5.1) + String leftValue = getLiteralOrFieldValue(outputMessage, inputMessage, leftOperand); + + // matches membership operator (e.g. MSH-5.1 in ('EPIC', 'CERNER')) + if (operator.equals("in")) { + return evaluateMembership(leftValue, rightOperand); + } + + // matches either a literal value (e.g. 'EPIC') or a field reference (e.g. MSH-5.1, + // input.MSH-5.1) + String rightValue = getLiteralOrFieldValue(outputMessage, inputMessage, rightOperand); + + // matches equality operators (e.g. MSH-5.1 = 'EPIC', MSH-5.1 != 'EPIC') + return evaluateEquality(leftValue, rightValue, operator); + } + + protected > boolean evaluateEquality( + T leftValue, T rightValue, String operator) { + if (operator.equals("=")) { + return leftValue.equals(rightValue); + } else if (operator.equals("!=")) { + return !leftValue.equals(rightValue); + } + throw new HL7ParserException("Unknown operator: " + operator); + } + + protected boolean evaluateMembership(String leftValue, String rightOperand) { + Matcher literalValueCollectionMatcher = + LITERAL_VALUE_COLLECTION_PATTERN.matcher(rightOperand); + if (!literalValueCollectionMatcher.matches()) { + throw new HL7ParserException("Invalid collection format: " + rightOperand); + } + String arrayString = literalValueCollectionMatcher.group(1); + Set values = + Arrays.stream(arrayString.split(",")) + .map(s -> s.trim().replace("'", "")) + .collect(Collectors.toSet()); + return values.contains(leftValue); + } + + protected boolean evaluateCollectionCount( + HL7Message message, String segmentName, String rightOperand, String operator) { + try { + int count = message.getSegmentCount(segmentName); + int rightValue = Integer.parseInt(rightOperand); + return evaluateEquality(count, rightValue, operator); + } catch (NumberFormatException e) { + throw new HL7ParserException( + "Error evaluating collection count. Segment: " + + segmentName + + ", count: " + + rightOperand, + e); + } + } + + protected String getLiteralOrFieldValue( + HL7Message outputMessage, HL7Message inputMessage, String operand) { + Matcher literalValueMatcher = LITERAL_VALUE_PATTERN.matcher(operand); + if (literalValueMatcher.matches()) { + return literalValueMatcher.group(1); + } + return getFieldValue(outputMessage, inputMessage, operand); + } + + protected String getFieldValue( + HL7Message outputMessage, HL7Message inputMessage, String fieldName) { + Matcher messageSourceMatcher = MESSAGE_SOURCE_PATTERN.matcher(fieldName); + if (!messageSourceMatcher.matches()) { + throw new HL7ParserException("Invalid field name format: " + fieldName); + } + + String fileSource = messageSourceMatcher.group(1); + String hl7Path = messageSourceMatcher.group(2); + HL7Message message = getMessageBySource(fileSource, inputMessage, outputMessage); + + try { + return message.getValue(hl7Path); + } catch (HL7MessageException e) { + return null; + } + } + + protected HL7Message getMessageBySource( + String source, HL7Message inputMessage, HL7Message outputMessage) { + if ("input".equals(source)) { + if (inputMessage == null) { + throw new HL7ParserException("Input message is null for: " + source); + } + return inputMessage; + } + return outputMessage; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcher.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcher.java new file mode 100644 index 000000000..b640ae415 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcher.java @@ -0,0 +1,75 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import com.google.common.collect.Sets; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The HL7FileMatcher class is responsible for matching input and output HL7 files based on the + * control ID (MSH-10). + */ +public class HL7FileMatcher { + + private static final HL7FileMatcher INSTANCE = new HL7FileMatcher(); + + private HL7FileMatcher() {} + + public static HL7FileMatcher getInstance() { + return INSTANCE; + } + + public Map matchFiles( + List outputFiles, List inputFiles) + throws HL7FileMatcherException { + // We pair up output and input files based on the control ID, which is in MSH-10 + // Any files (either input or output) that don't have a match are logged + Map inputMap = parseAndMapMessageByControlId(inputFiles); + Map outputMap = parseAndMapMessageByControlId(outputFiles); + + Set inputKeys = inputMap.keySet(); + Set outputKeys = outputMap.keySet(); + Set unmatchedKeys = new HashSet<>(); + unmatchedKeys.addAll(Sets.difference(inputKeys, outputKeys)); + unmatchedKeys.addAll(Sets.difference(outputKeys, inputKeys)); + + if (!unmatchedKeys.isEmpty()) { + throw new HL7FileMatcherException( + "Found no match for messages with the following MSH-10 values: " + + unmatchedKeys); + } + + return inputKeys.stream().collect(Collectors.toMap(inputMap::get, outputMap::get)); + } + + public Map parseAndMapMessageByControlId(List files) + throws HL7FileMatcherException { + + Map messageMap = new HashMap<>(); + + for (HL7FileStream hl7FileStream : files) { + String fileName = hl7FileStream.fileName(); + try (InputStream inputStream = hl7FileStream.inputStream()) { + String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + HL7Message message = HL7Message.parse(content); + String msh10 = message.getIdentifier(); + if (msh10 == null || msh10.isEmpty()) { + throw new HL7FileMatcherException( + String.format("MSH-10 is empty for file: %s", fileName)); + } + messageMap.put(msh10, message); + } catch (IOException e) { + throw new HL7FileMatcherException( + String.format("Failed to read file: %s", fileName), e); + } + } + + return messageMap; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherException.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherException.java new file mode 100644 index 000000000..99e5de7f7 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherException.java @@ -0,0 +1,16 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +/** + * The HL7FileMatcherException class is responsible for handling exceptions that occur in the + * HL7FileMatcher class. + */ +public class HL7FileMatcherException extends Exception { + + public HL7FileMatcherException(String message, Throwable cause) { + super(message, cause); + } + + public HL7FileMatcherException(String message) { + super(message); + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileStream.java similarity index 81% rename from rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java rename to rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileStream.java index 6064c9d13..1ac74e436 100644 --- a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/HL7FileStream.java +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileStream.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.rse2e; +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; import java.io.InputStream; diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Message.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Message.java new file mode 100644 index 000000000..f89cb0f78 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Message.java @@ -0,0 +1,141 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import gov.hhs.cdc.trustedintermediary.wrappers.HealthData; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Represents a HL7 message that implements the HealthData interface and adds methods to access the + * HL7 data + */ +public class HL7Message implements HealthData { + + static final String MSH_SEGMENT_NAME = "MSH"; + static final String NEWLINE_REGEX = "\\r?\\n|\\r"; + + private final List segments; + private final HL7Encoding encoding; + + HL7Message(List segments, HL7Encoding encoding) { + this.segments = segments; + this.encoding = encoding; + } + + @Override + public HL7Message getUnderlyingData() { + return this; + } + + @Override + public String getIdentifier() { + try { + return getValue("MSH-10"); + } catch (HL7MessageException e) { + return null; + } + } + + @Override + public String toString() { + return String.join( + HL7Encoding.DEFAULT_SEGMENT_DELIMITER, + getSegments().stream() + .map(segment -> segmentToString(segment, this.encoding)) + .toList()); + } + + public static HL7Message parse(String content) { + List segments = new ArrayList<>(); + String encodingCharactersField = null; + String[] lines = content.split(NEWLINE_REGEX); + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] fields = + line.split( + Pattern.quote(String.valueOf(HL7Encoding.DEFAULT_FIELD_DELIMITER)), -1); + String segmentName = fields[0]; + List segmentFields = + new ArrayList<>(Arrays.asList(fields).subList(1, fields.length)); + if (Objects.equals(segmentName, MSH_SEGMENT_NAME)) { + encodingCharactersField = fields[1]; + segmentFields.add(0, String.valueOf(HL7Encoding.DEFAULT_FIELD_DELIMITER)); + } + segments.add(new HL7Segment(segmentName, segmentFields)); + } + + return new HL7Message(segments, HL7Encoding.fromEncodingField(encodingCharactersField)); + } + + public List getSegments() { + return this.segments; + } + + public HL7Encoding getEncoding() { + return this.encoding; + } + + public List getSegments(String name) { + return this.segments.stream().filter(segment -> segment.name().equals(name)).toList(); + } + + public int getSegmentCount(String name) { + return getSegments(name).size(); + } + + public boolean hasSegment(String name, int index) { + return getSegmentCount(name) > index; + } + + public HL7Segment getSegment(String name, int index) throws HL7MessageException { + if (!hasSegment(name, index)) { + throw new HL7MessageException( + String.format("Segment %s at index %d not found", name, index)); + } + return getSegments(name).get(index); + } + + public HL7Segment getSegment(String name) throws HL7MessageException { + return getSegment(name, 0); + } + + public String getValue(String path) throws HL7MessageException { + HL7Path hl7Path = HL7Path.parse(path); + int[] indices = hl7Path.indices(); + List fields = this.getSegment(hl7Path.segmentName()).fields(); + char[] delimiters = this.getEncoding().getOrderedDelimiters(); + + if (indices[0] > fields.size()) { + return ""; + } + + String value = fields.get(indices[0] - 1); + for (int i = 1; i < indices.length; i++) { + if (i >= delimiters.length) { + throw new HL7ParserException("Invalid HL7 path: too many sub-levels provided"); + } + char segmentDelimiter = delimiters[i]; + int index = indices[i] - 1; + String[] parts = value.split(Pattern.quote(String.valueOf(segmentDelimiter))); + if (index < 0 || index >= parts.length) { + return ""; + } + value = parts[index]; + } + return value; + } + + protected static String segmentToString(HL7Segment segment, HL7Encoding encoding) { + String fieldSeparator = String.valueOf(encoding.getFieldDelimiter()); + + if (segment.name().equals(MSH_SEGMENT_NAME)) { + return segment.name() + + segment.fields().get(0) + + String.join( + fieldSeparator, segment.fields().subList(1, segment.fields().size())); + } + return segment.name() + fieldSeparator + String.join(fieldSeparator, segment.fields()); + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageException.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageException.java new file mode 100644 index 000000000..773888711 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageException.java @@ -0,0 +1,12 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +/** + * The HL7MessageException class is responsible for handling exceptions that occur in the HL7Message + * class. + */ +public class HL7MessageException extends Exception { + + public HL7MessageException(String message) { + super(message); + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ParserException.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ParserException.java new file mode 100644 index 000000000..e5c549b53 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ParserException.java @@ -0,0 +1,16 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +/** + * The HL7ParserException class is responsible for handling exceptions that occur in the HL7Parser + * and HL7ExpressionEvaluator class. + */ +public class HL7ParserException extends RuntimeException { + + public HL7ParserException(String message, Throwable cause) { + super(message, cause); + } + + public HL7ParserException(String message) { + super(message); + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Path.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Path.java new file mode 100644 index 000000000..b8eda42e7 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Path.java @@ -0,0 +1,43 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** The HL7Path class represents a path to a specific field in an HL7 message. */ +public record HL7Path(String segmentName, int[] indices) { + static final Pattern HL7_FIELD_NAME_PATTERN = Pattern.compile("(\\w+)-(\\d+(?:\\.\\d+)*)"); + + public static HL7Path parse(String path) { + Matcher matcher = HL7_FIELD_NAME_PATTERN.matcher(path); + if (!matcher.matches()) { + throw new HL7ParserException("Invalid HL7 path format: " + path); + } + + String segmentName = matcher.group(1); + int[] indices = + Arrays.stream(matcher.group(2).split("\\.")).mapToInt(Integer::parseInt).toArray(); + + return new HL7Path(segmentName, indices); + } + + // Need to override equals, hashCode, and toString to handle array comparison + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof HL7Path hl7Path)) return false; + return (Objects.equals(segmentName, hl7Path.segmentName)) + && Arrays.equals(indices, hl7Path.indices); + } + + @Override + public int hashCode() { + return Objects.hash(segmentName, Arrays.hashCode(indices)); + } + + @Override + public String toString() { + return "HL7Path[segmentName=" + segmentName + ", indices=" + Arrays.toString(indices) + "]"; + } +} diff --git a/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Segment.java b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Segment.java new file mode 100644 index 000000000..ec40cdc91 --- /dev/null +++ b/rs-e2e/src/main/java/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7Segment.java @@ -0,0 +1,6 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7; + +import java.util.List; + +/** The HL7Segment class represents an HL7 segment and its fields. */ +public record HL7Segment(String name, List fields) {} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy index c1a4d45ab..125d6c2ed 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AutomatedTest.groovy @@ -1,9 +1,12 @@ package gov.hhs.cdc.trustedintermediary.rse2e import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7FileMatcher -import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7ExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.rse2e.external.azure.AzureBlobFileFetcher +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7FileStream +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7FileMatcher +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7ExpressionEvaluator import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson +import gov.hhs.cdc.trustedintermediary.rse2e.external.localfile.LocalFileFetcher import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.Logger import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter @@ -16,13 +19,13 @@ class AutomatedTest extends Specification { List azureFiles List localFiles AssertionRuleEngine engine - HapiHL7FileMatcher fileMatcher + HL7FileMatcher fileMatcher Logger mockLogger = Mock(Logger) List loggedErrorsAndWarnings = [] def setup() { engine = AssertionRuleEngine.getInstance() - fileMatcher = HapiHL7FileMatcher.getInstance() + fileMatcher = HL7FileMatcher.getInstance() TestApplicationContext.reset() TestApplicationContext.init() @@ -30,8 +33,8 @@ class AutomatedTest extends Specification { TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) TestApplicationContext.register(Logger, mockLogger) TestApplicationContext.register(Formatter, Jackson.getInstance()) - TestApplicationContext.register(HapiHL7FileMatcher, fileMatcher) - TestApplicationContext.register(HealthDataExpressionEvaluator, HapiHL7ExpressionEvaluator.getInstance()) + TestApplicationContext.register(HL7FileMatcher, fileMatcher) + TestApplicationContext.register(HealthDataExpressionEvaluator, HL7ExpressionEvaluator.getInstance()) TestApplicationContext.register(LocalFileFetcher, LocalFileFetcher.getInstance()) TestApplicationContext.injectRegisteredImplementations() diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelperTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelperTest.groovy similarity index 93% rename from rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelperTest.groovy rename to rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelperTest.groovy index 29b39b674..42ec02e2e 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/AzureBlobHelperTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/azure/AzureBlobHelperTest.groovy @@ -1,4 +1,5 @@ -package gov.hhs.cdc.trustedintermediary.rse2e +package gov.hhs.cdc.trustedintermediary.rse2e.external.azure + import spock.lang.Specification diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy deleted file mode 100644 index c7df2dd72..000000000 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7MessageTest.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi - -import ca.uhn.hl7v2.model.Message -import ca.uhn.hl7v2.model.v251.message.ORU_R01 -import ca.uhn.hl7v2.model.v251.segment.MSH -import spock.lang.Specification - -class HapiHL7MessageTest extends Specification { - - def "getUnderlyingData should correctly initialize and return underlying data"() { - given: - def mockMessage = Mock(Message) - def hl7Message = new HapiHL7Message(mockMessage) - - expect: - hl7Message.getUnderlyingData() == mockMessage - } - - def "getIdentifier should return the MSH-10 identifier of the underlying message"() { - given: - def expectedIdentifier = "0001" - def oruMessage = new ORU_R01() - MSH mshSegment = oruMessage.getMSH() - mshSegment.getMessageControlID().setValue(expectedIdentifier) - - and: - def hl7Message = new HapiHL7Message(oruMessage) - - when: - def name = hl7Message.getIdentifier() - - then: - name == expectedIdentifier - } -} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7EncodingTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7EncodingTest.groovy new file mode 100644 index 000000000..17ef6ef81 --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7EncodingTest.groovy @@ -0,0 +1,55 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 + +import spock.lang.Specification + +class HL7EncodingTest extends Specification { + + def "defaultEncoding should use expected default characters"() { + when: + def encodingChars = HL7Encoding.defaultEncoding() + + then: + encodingChars.getFieldDelimiter() == '|' as char + encodingChars.getComponentDelimiter() == '^' as char + encodingChars.getRepetitionDelimiter() == '~' as char + encodingChars.getEscapeCharacter() == '\\' as char + encodingChars.getSubcomponentDelimiter() == '&' as char + } + + def "fromEncodingField should use custom encoding characters when provided"() { + given: + def customEncodingChars = "@#+_" + + when: + def encodingChars = HL7Encoding.fromEncodingField(customEncodingChars) + + then: + encodingChars.getFieldDelimiter() == '|' as char + encodingChars.getComponentDelimiter() == '@' as char + encodingChars.getRepetitionDelimiter() == '#' as char + encodingChars.getEscapeCharacter() == '+' as char + encodingChars.getSubcomponentDelimiter() == '_' as char + } + + def "fromEncodingField should use default characters when encoding characters passed is blank or null"() { + when: + def blankEncodingChars = HL7Encoding.fromEncodingField("") + + then: + blankEncodingChars.getFieldDelimiter() == '|' as char + blankEncodingChars.getComponentDelimiter() == '^' as char + blankEncodingChars.getRepetitionDelimiter() == '~' as char + blankEncodingChars.getEscapeCharacter() == '\\' as char + blankEncodingChars.getSubcomponentDelimiter() == '&' as char + + when: + def nullEncodingChars = HL7Encoding.fromEncodingField(null) + + then: + nullEncodingChars.getFieldDelimiter() == '|' as char + nullEncodingChars.getComponentDelimiter() == '^' as char + nullEncodingChars.getRepetitionDelimiter() == '~' as char + nullEncodingChars.getEscapeCharacter() == '\\' as char + nullEncodingChars.getSubcomponentDelimiter() == '&' as char + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluatorTest.groovy similarity index 55% rename from rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy rename to rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluatorTest.groovy index aaba6b9d7..b87c17c94 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7ExpressionEvaluatorTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7ExpressionEvaluatorTest.groovy @@ -1,46 +1,36 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 -import ca.uhn.hl7v2.model.Message -import ca.uhn.hl7v2.model.Segment -import ca.uhn.hl7v2.parser.PipeParser import gov.hhs.cdc.trustedintermediary.wrappers.HealthData import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext import spock.lang.Specification -class HapiHL7ExpressionEvaluatorTest extends Specification { +class HL7ExpressionEvaluatorTest extends Specification { - def evaluator = HapiHL7ExpressionEvaluator.getInstance() + def evaluator = HL7ExpressionEvaluator.getInstance() - char hl7FieldSeparator = '|' - String hl7FieldEncodingCharacters = "^~\\&" - Message mshMessage - Segment mshSegment - String mshSegmentText + HL7Message hl7Message + String messageContent def setup() { TestApplicationContext.reset() TestApplicationContext.init() - TestApplicationContext.register(HapiHL7ExpressionEvaluator, evaluator) + TestApplicationContext.register(HL7ExpressionEvaluator, evaluator) - mshSegmentText = "MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|111111|T|2.5.1" - def pipeParser = new PipeParser() - mshMessage = pipeParser.parse(mshSegmentText) - mshSegment = (Segment) mshMessage.get("MSH") + messageContent = """MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|111111|T|2.5.1 +PID|1||11102779^^^CR^MR||SMITH^BB SARAH^^^^^L""" + hl7Message = HL7Message.parse(messageContent) TestApplicationContext.injectRegisteredImplementations() } def "evaluateExpression returns boolean when evaluating valid assertions"() { given: - def spyEvaluator = Spy(HapiHL7ExpressionEvaluator.getInstance()) - spyEvaluator.getLiteralOrFieldValue(_ as Message, _ as Message, _ as String) >> "mockedValue" + def spyEvaluator = Spy(HL7ExpressionEvaluator.getInstance()) + spyEvaluator.getLiteralOrFieldValue(_ as HL7Message, _ as HL7Message, _ as String) >> "mockedValue" spyEvaluator.evaluateEquality(_ as String, _ as String, _ as String) >> true spyEvaluator.evaluateMembership(_ as String, _ as String) >> true - spyEvaluator.evaluateCollectionCount(_ as Message, _ as String, _ as String, _ as String) >> true - - def healthData = Mock(HealthData) { - getUnderlyingData() >> Mock(Message) - } + spyEvaluator.evaluateCollectionCount(_ as HL7Message, _ as String, _ as String, _ as String) >> true + def healthData = Mock(HL7Message) expect: spyEvaluator.evaluateExpression(assertion, healthData, healthData) @@ -63,15 +53,12 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def "evaluateExpression allows null input message when no assertions use input"() { given: - def spyEvaluator = Spy(HapiHL7ExpressionEvaluator.getInstance()) - spyEvaluator.getLiteralOrFieldValue(_ as Message, null, _ as String) >> "mockedValue" + def spyEvaluator = Spy(HL7ExpressionEvaluator.getInstance()) + spyEvaluator.getLiteralOrFieldValue(_ as HL7Message, null, _ as String) >> "mockedValue" spyEvaluator.evaluateEquality(_ as String, _ as String, _ as String) >> true spyEvaluator.evaluateMembership(_ as String, _ as String) >> true - spyEvaluator.evaluateCollectionCount(_ as Message, _ as String, _ as String, _ as String) >> true - - def healthData = Mock(HealthData) { - getUnderlyingData() >> Mock(Message) - } + spyEvaluator.evaluateCollectionCount(_ as HL7Message, _ as String, _ as String, _ as String) >> true + def healthData = Mock(HL7Message) expect: spyEvaluator.evaluateExpression(assertion, healthData) @@ -90,7 +77,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { evaluator.evaluateExpression("invalid format", Mock(HealthData)) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Invalid statement format") } @@ -99,7 +86,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { evaluator.evaluateExpression("'EPIC' = 'EPIC'", Mock(HealthData), Mock(HealthData), Mock(HealthData)) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Expected two messages") } @@ -111,7 +98,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { evaluator.evaluateExpression(condition, Mock(HealthData)) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Invalid statement format") } @@ -152,7 +139,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { evaluator.evaluateEquality("left", "right", unknownOperator) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Unknown operator") } @@ -183,7 +170,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { evaluator.evaluateMembership("value", invalidSet) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Invalid collection format") } @@ -194,7 +181,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def operator = "=" when: - def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + def result = evaluator.evaluateCollectionCount(hl7Message, segmentName, rightOperand, operator) then: result @@ -207,7 +194,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def operator = "=" when: - def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + def result = evaluator.evaluateCollectionCount(hl7Message, segmentName, rightOperand, operator) then: !result @@ -220,10 +207,10 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def operator = "=" when: - evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + evaluator.evaluateCollectionCount(hl7Message, segmentName, rightOperand, operator) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getCause().getClass() == NumberFormatException } @@ -234,7 +221,7 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def operator = "=" when: - def result = evaluator.evaluateCollectionCount(mshMessage, segmentName, rightOperand, operator) + def result = evaluator.evaluateCollectionCount(hl7Message, segmentName, rightOperand, operator) then: !result @@ -243,8 +230,8 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def "getLiteralOrFieldValue returns literal value when literal is specified"() { given: def operand = "'Epic'" - def inputMessage = Mock(Message) - def outputMessage = Mock(Message) + def inputMessage = Mock(HL7Message) + def outputMessage = Mock(HL7Message) when: def result = evaluator.getLiteralOrFieldValue(outputMessage, inputMessage, operand) @@ -256,164 +243,82 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def "getLiteralOrFieldValue returns field value when field is specified"() { given: def operand = "MSH-3" - def inputMessage = Mock(Message) + def inputMessage = Mock(HL7Message) def msh3 = "Sender Application^sender.test.com^DNS" when: - def result = evaluator.getLiteralOrFieldValue(mshMessage, inputMessage, operand) + def result = evaluator.getLiteralOrFieldValue(hl7Message, inputMessage, operand) then: result == msh3 } - def "getFieldValue returns specified field value"() { + def "getLiteralOrFieldValue throws exception when using invalid operand"() { given: - def fieldName = "MSH-3" - def msh3 = "Sender Application^sender.test.com^DNS" - def inputMessage = Mock(Message) + def operand = "invalidField" + def inputMessage = Mock(HL7Message) when: - def result = evaluator.getFieldValue(mshMessage, inputMessage, fieldName) + evaluator.getLiteralOrFieldValue(hl7Message, inputMessage, operand) then: - result == msh3 + thrown(HL7ParserException) } - def "getFieldValue throws exception for non numeric field index"() { - given: - def fieldName = "MSH-three" - def inputMessage = Mock(Message) - - when: - evaluator.getFieldValue(mshMessage, inputMessage, fieldName) - - then: - def e = thrown(IllegalArgumentException) - e.getCause().getClass() == NumberFormatException - } - - def "getFieldValue throws exception for empty field name"() { - given: - def fieldName = "" - def inputMessage = Mock(Message) - - when: - evaluator.getFieldValue(mshMessage, inputMessage, fieldName) - - then: - def e = thrown(IllegalArgumentException) - e.getMessage().contains("Invalid field name format") - } - - def "getSegmentFieldValue should return segment when field components indicate segment"() { - given: - def fieldName = "MSH" - - when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) - - then: - result == mshSegmentText - } - - def "getSegmentFieldValue should return field when field components indicate field"() { + def "getFieldValue returns specified field value"() { given: def fieldName = "MSH-3" def msh3 = "Sender Application^sender.test.com^DNS" + def inputMessage = Mock(HL7Message) when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + def result = evaluator.getFieldValue(hl7Message, inputMessage, fieldName) then: result == msh3 } - def "getSegmentFieldValue should return subfield when field components indicate subfield"() { - given: - def fieldName = "MSH-3.2" - def msh32 = "sender.test.com" - - when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) - - then: - result == msh32 - } - - def "getSegmentFieldValue should return empty string when field components indicate subfield but subfield not present"() { + def "getFieldValue throws exception for non numeric field index"() { given: - def fieldName = "MSH-3.4" + def fieldName = "MSH-three" + def inputMessage = Mock(HL7Message) when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + evaluator.getFieldValue(hl7Message, inputMessage, fieldName) then: - result == "" + thrown(HL7ParserException) } - def "getSegmentFieldValue returns null when looking for segment that isn't in message"() { + def "getFieldValue returns null for fields that don't exist"() { given: - def fieldName = "OBX" + def fieldName = "ZZZ-1" + def inputMessage = Mock(HL7Message) when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + def result = evaluator.getFieldValue(hl7Message, inputMessage, fieldName) then: result == null } - def "getSegmentFieldValue throws exception when field name is invalid"() { - given: - def fieldName = "MSH-" - - when: - evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) - - then: - def e = thrown(IllegalArgumentException) - e.getMessage().contains("Invalid HL7 field format: ") - } - - def "getSegmentFieldValue throws exception when field index is out of bounds"() { - given: - def fieldName = "MSH-99" - - when: - evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) - - then: - def e = thrown(IllegalArgumentException) - e.getMessage().contains("Invalid field index (out of bounds)") - } - - def "getSegmentFieldValue throws exception when there are too many subfield levels"() { - given: - def fieldName = "MSH-3.3.3.3.3.3" - - when: - evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) - - then: - def e = thrown(IllegalArgumentException) - e.getMessage().contains("Invalid subfield index (out of bounds)") - } - - def "getSegmentFieldValue returns empty string when sub-field index is out of bounds"() { + def "getFieldValue throws exception for empty field name"() { given: - def fieldName = "MSH-3.99" + def fieldName = "" + def inputMessage = Mock(HL7Message) when: - def result = evaluator.getSegmentFieldValue(mshSegmentText, fieldName, hl7FieldSeparator, hl7FieldEncodingCharacters) + evaluator.getFieldValue(hl7Message, inputMessage, fieldName) then: - result == "" + thrown(HL7ParserException) } def "getMessageBySource should return input message when source is input"() { given: def source = "input" - def inputMessage = Mock(Message) - def outputMessage = Mock(Message) + def inputMessage = Mock(HL7Message) + def outputMessage = Mock(HL7Message) when: def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) @@ -425,8 +330,8 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def "getMessageBySource should return output message when source is not input"() { given: def source = "output" - def inputMessage = Mock(Message) - def outputMessage = Mock(Message) + def inputMessage = Mock(HL7Message) + def outputMessage = Mock(HL7Message) when: def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) @@ -438,8 +343,8 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { def "getMessageBySource should return output message when source is empty"() { given: def source = "" - def inputMessage = Mock(Message) - def outputMessage = Mock(Message) + def inputMessage = Mock(HL7Message) + def outputMessage = Mock(HL7Message) when: def result = evaluator.getMessageBySource(source, inputMessage, outputMessage) @@ -452,13 +357,13 @@ class HapiHL7ExpressionEvaluatorTest extends Specification { given: def source = "input" def inputMessage = null - def outputMessage = Mock(Message) + def outputMessage = Mock(HL7Message) when: evaluator.getMessageBySource(source, inputMessage, outputMessage) then: - def e = thrown(IllegalArgumentException) + def e = thrown(HL7ParserException) e.getMessage().contains("Input message is null for") } } diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherExceptionTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherExceptionTest.groovy similarity index 65% rename from rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherExceptionTest.groovy rename to rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherExceptionTest.groovy index 07e65529e..c4ed62ac5 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherExceptionTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherExceptionTest.groovy @@ -1,8 +1,9 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 + import spock.lang.Specification -class HapiHL7FileMatcherExceptionTest extends Specification { +class HL7FileMatcherExceptionTest extends Specification { def "two param constructor works"() { given: @@ -10,7 +11,7 @@ class HapiHL7FileMatcherExceptionTest extends Specification { def cause = new NullPointerException() when: - def exception = new HapiHL7FileMatcherException(message, cause) + def exception = new HL7FileMatcherException(message, cause) then: exception.getMessage() == message @@ -22,7 +23,7 @@ class HapiHL7FileMatcherExceptionTest extends Specification { def message = "something blew up!" when: - def exception = new HapiHL7FileMatcherException(message) + def exception = new HL7FileMatcherException(message) then: exception.getMessage() == message diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherTest.groovy similarity index 72% rename from rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy rename to rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherTest.groovy index c00789a4d..0773388bb 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/external/hapi/HapiHL7FileMatcherTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7FileMatcherTest.groovy @@ -1,27 +1,26 @@ -package gov.hhs.cdc.trustedintermediary.rse2e.external.hapi +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.rse2e.HL7FileStream import gov.hhs.cdc.trustedintermediary.wrappers.Logger import spock.lang.Specification -class HapiHL7FileMatcherTest extends Specification { +class HL7FileMatcherTest extends Specification { def mockLogger = Mock(Logger) - def fileMatcher = HapiHL7FileMatcher.getInstance() + def fileMatcher = HL7FileMatcher.getInstance() def setup() { TestApplicationContext.reset() TestApplicationContext.init() TestApplicationContext.register(Logger, mockLogger) - TestApplicationContext.register(HapiHL7FileMatcher, fileMatcher) + TestApplicationContext.register(HL7FileMatcher, fileMatcher) TestApplicationContext.injectRegisteredImplementations() } def "should correctly match input and output files"() { given: - def spyFileMatcher = Spy(HapiHL7FileMatcher.getInstance()) + def spyFileMatcher = Spy(HL7FileMatcher.getInstance()) def mockInputFiles = [ new HL7FileStream("inputFileStream1", Mock(InputStream)), new HL7FileStream("inputFileStream2", Mock(InputStream)) @@ -30,10 +29,10 @@ class HapiHL7FileMatcherTest extends Specification { new HL7FileStream("outputFileStream1", Mock(InputStream)), new HL7FileStream("outputFileStream2", Mock(InputStream)) ] - def mockInputMessage1 = Mock(HapiHL7Message) - def mockInputMessage2 = Mock(HapiHL7Message) - def mockOutputMessage1 = Mock(HapiHL7Message) - def mockOutputMessage2 = Mock(HapiHL7Message) + def mockInputMessage1 = Mock(HL7Message) + def mockInputMessage2 = Mock(HL7Message) + def mockOutputMessage1 = Mock(HL7Message) + def mockOutputMessage2 = Mock(HL7Message) spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": mockInputMessage1, "002": mockInputMessage2 ] spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": mockOutputMessage1, "002": mockOutputMessage2 ] @@ -46,11 +45,11 @@ class HapiHL7FileMatcherTest extends Specification { } - def "should throw HapiHL7FileMatcherException if didn't find a match for at least one file in either input or output"() { + def "should throw HL7FileMatcherException if didn't find a match for at least one file in either input or output"() { given: def mockInputFiles def mockOutputFiles - def spyFileMatcher = Spy(HapiHL7FileMatcher.getInstance()) + def spyFileMatcher = Spy(HL7FileMatcher.getInstance()) when: mockInputFiles = [ @@ -60,12 +59,12 @@ class HapiHL7FileMatcherTest extends Specification { mockOutputFiles = [ new HL7FileStream("matchingOutputFileStream", Mock(InputStream)) ] - spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HapiHL7Message), "002": Mock(HapiHL7Message) ] - spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HapiHL7Message) ] + spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HL7Message), "002": Mock(HL7Message) ] + spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HL7Message) ] spyFileMatcher.matchFiles(mockOutputFiles, mockInputFiles) then: - def nonMatchingInputException = thrown(HapiHL7FileMatcherException) + def nonMatchingInputException = thrown(HL7FileMatcherException) with(nonMatchingInputException.getMessage()) { contains("Found no match") contains("002") @@ -79,12 +78,12 @@ class HapiHL7FileMatcherTest extends Specification { new HL7FileStream("matchingOutputFileStream", Mock(InputStream)), new HL7FileStream("nonMatchingOutputFileStream", Mock(InputStream)) ] - spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HapiHL7Message) ] - spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HapiHL7Message), "003": Mock(HapiHL7Message) ] + spyFileMatcher.parseAndMapMessageByControlId(mockInputFiles) >> ["001": Mock(HL7Message) ] + spyFileMatcher.parseAndMapMessageByControlId(mockOutputFiles) >> ["001": Mock(HL7Message), "003": Mock(HL7Message) ] spyFileMatcher.matchFiles(mockOutputFiles, mockInputFiles) then: - def nonMatchingOutputException = thrown(HapiHL7FileMatcherException) + def nonMatchingOutputException = thrown(HL7FileMatcherException) with(nonMatchingOutputException.getMessage()) { contains("Found no match") contains("003") @@ -117,11 +116,11 @@ class HapiHL7FileMatcherTest extends Specification { def message2 = result[file2Msh10] message1 != null message2 != null - file1MshSegment == message1.getUnderlyingData().encode().trim() - file2MshSegment == message2.getUnderlyingData().encode().trim() + file1MshSegment == message1.getUnderlyingData().toString() + file2MshSegment == message2.getUnderlyingData().toString() } - def "should throw HapiHL7FileMatcherException when MSH-10 is empty"() { + def "should throw HL7FileMatcherException when MSH-10 is empty"() { given: def msh1to9 = "MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^simulated-lab-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|" def msh11to12 = "|T|2.5.1" @@ -134,10 +133,10 @@ class HapiHL7FileMatcherTest extends Specification { fileMatcher.parseAndMapMessageByControlId([hl7FileStream]) then: - thrown(HapiHL7FileMatcherException) + thrown(HL7FileMatcherException) } - def "should throw HapiHL7FileMatcherException when not able to parse the file as HL7 message"() { + def "should throw HL7FileMatcherException when not able to parse the file as HL7 message"() { given: def inputStream = new ByteArrayInputStream("".bytes) def hl7FileStream = new HL7FileStream("badFile", inputStream) @@ -146,6 +145,20 @@ class HapiHL7FileMatcherTest extends Specification { fileMatcher.parseAndMapMessageByControlId([hl7FileStream]) then: - thrown(HapiHL7FileMatcherException) + thrown(HL7FileMatcherException) + } + + def "should throw HL7FileMatcherException when unable to load a file to parse and map"() { + def inputStream = Mock(ByteArrayInputStream) + inputStream.readAllBytes() >> { + throw new IOException("") + } + def hl7FileStream = new HL7FileStream("file1", inputStream) + + when: + fileMatcher.parseAndMapMessageByControlId([hl7FileStream]) + + then: + thrown(HL7FileMatcherException) } } diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageTest.groovy new file mode 100644 index 000000000..abddd47fd --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7MessageTest.groovy @@ -0,0 +1,155 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 + +import spock.lang.Specification + +class HL7MessageTest extends Specification { + + HL7Message message + String messageContent + + def setup() { + messageContent = """MSH|^~\\&|Sender Application^sender.test.com^DNS|Sender Facility^0.0.0.0.0.0.0.0^ISO|Receiver Application^0.0.0.0.0.0.0.0^ISO|Receiver Facility^automated-staging-test-receiver-id^DNS|20230101010000-0000||ORM^O01^ORM_O01|001|N|2.5.1|||||||||| +PID|1||1300974^^^Baptist East^MR||ONE^TESTCASE||202402210152-0500|F^Female^HL70001||2106-3^White^HL70005|1234 GPCS WAY^^MONTGOMERY^Alabama^36117^USA^home^^Montgomery|||||||2227600015||||N^Not Hispanic or Latino^HL70189|||1|||||||||||||||LRI_NG_FRN_PROFILE^^2.16.840.1.113883.9.195.3.4^ISO~LRI_NDBS_COMPONENT^^2.16.840.1.113883.9.195.3.6^ISO~LAB_PRN_Component^^2.16.840.1.113883.9.81^ISO~LAB_TO_COMPONENT^^2.16.840.1.113883.9.22^ISO| +NK1|1|ONE^MOMFIRST|MTH^Mother^HL70063||^^^^^804^5693861||||||||||||||||||||||||||||123456789^^^Medicaid&2.16.840.1.113883.4.446&ISO^MD||||000-00-0000^^^ssn&2.16.840.1.113883.4.1&ISO^SS +ORC|NW|4560411583^ORDERID||||||||||12345^^^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L||||||||| +OBR|1|4560411583^ORDERID||54089-8^Newborn screening panel AHIC^LN|||202402221854-0500|||||||||12345^^^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L|||||||| +OBX|1|ST|57723-9^Unique bar code number of Current sample^LN||123456||||||F|||202402221854-0500 +OBX|2|NM|||3122||||||F|||202402221854-0500||""" + message = HL7Message.parse(messageContent) + } + + def "parse should handle basic HL7 message"() { + given: + def content = """MSH|^~\\&|sending_app|sending_facility +PID|||12345||Doe^John||19800101|M""" + + when: + def message = HL7Message.parse(content) + def segments = message.getSegments() + + then: + segments.size() == 2 + segments[0].name() == "MSH" + segments[0].fields().size() == 4 + segments[0].fields().get(0) == "|" + segments[0].fields().get(1) == "^~\\&" + segments[1].name() == "PID" + segments[1].fields().size() == 8 + segments[1].fields().get(2) == "12345" + segments[1].fields().get(4) == "Doe^John" + } + + def "parse should handle empty lines in message"() { + given: + def content = """MSH|^~\\&|sending_app + +PID|||12345""" + + when: + def result = HL7Message.parse(content) + + then: + result.getSegments().size() == 2 + } + + def "parse should preserve empty fields"() { + given: + def content = "MSH|^~\\&|sending_app||sending_facility" + + when: + def result = HL7Message.parse(content) + + then: + result.getSegments().get(0).fields().get(3) == "" + } + + def "getUnderlyingData should correctly return itself"() { + expect: + message.getUnderlyingData() == message + } + + def "toString should return the original string content"() { + expect: + message.toString() == messageContent + } + + def "getIdentifier should return the MSH-10 identifier of the underlying message"() { + expect: + message.getIdentifier() == "001" + } + + def "getSegments returns the expected segments"() { + when: + def actualSegment = message.getSegments("OBX").get(1) + + then: + actualSegment.name() == "OBX" + actualSegment.fields().get(0) == "2" + } + + def "getSegments returns all segments if no called with no arguments"() { + expect: + message.getSegments().size() == 7 + } + + def "getSegmentCount returns the expected number of segments"() { + expect: + message.getSegmentCount("MSH") == 1 + message.getSegmentCount("PID") == 1 + message.getSegmentCount("OBX") == 2 + } + + def "hasSegment returns expected boolean"() { + expect: + message.hasSegment("MSH", 0) + message.hasSegment("PID", 0) + message.hasSegment("OBX", 0) + message.hasSegment("OBX", 1) + !message.hasSegment("MSH", 1) + !message.hasSegment("ZZZ", 0) + } + + def "getSegment returns the expected segment"() { + when: + def obxSegment1 = message.getSegment("OBX") + def obxSegment2 = message.getSegment("OBX", 1) + + then: + obxSegment1.name() == "OBX" + obxSegment1.fields().get(0) == "1" + obxSegment2.name() == "OBX" + obxSegment2.fields().get(0) == "2" + } + + def "getValue returns the expected value given the segment name and indices"() { + expect: + message.getValue("PID-3") == "1300974^^^Baptist East^MR" + message.getValue("PID-3.0") == "" + message.getValue("PID-3.1") == "1300974" + message.getValue("PID-3.2") == "" + message.getValue("PID-3.4") == "Baptist East" + message.getValue("PID-3.5") == "MR" + message.getValue("PID-3.6") == "" + message.getValue("PID-40.4.1") == "ISO" + message.getValue("PID-40.4.2") == "LRI_NDBS_COMPONENT" + message.getValue("PID-40.4.3") == "" + message.getValue("NK1-33.4.1.1") == "Medicaid" + message.getValue("PID-99") == "" + } + + def "getValue should throws an exception when the hl7 field index has too many sublevels"() { + when: + message.getValue("NK1-33.4.1.1.1") + + then: + thrown(HL7ParserException) + } + + def "getValue should throws an exception when the segment is not found"() { + when: + message.getValue("ZZZ-1") + + then: + thrown(HL7MessageException) + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7PathTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7PathTest.groovy new file mode 100644 index 000000000..d866babac --- /dev/null +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/hl7/HL7PathTest.groovy @@ -0,0 +1,61 @@ +package gov.hhs.cdc.trustedintermediary.rse2e.hl7 + +import spock.lang.Specification + +class HL7PathTest extends Specification { + + def "should create HL7Path with segment name and indices"() { + when: + def path = new HL7Path("MSH", [1, 2, 3] as int[]) + + then: + path.segmentName() == "MSH" + path.indices() == [1, 2, 3] as int[] + } + + def "equals should compare array contents"() { + given: + def path1 = new HL7Path("MSH", [1, 2] as int[]) + def path2 = new HL7Path("MSH", [1, 2] as int[]) + def path3 = new HL7Path("MSH", [1, 3] as int[]) + + expect: + path1.equals(path2) + !path1.equals(path3) + !path1.equals(null) + path1.equals(path1) + } + + def "equals should handle different segment names"() { + given: + def path1 = new HL7Path(segment1, [1, 2] as int[]) + def path2 = new HL7Path(segment2, [1, 2] as int[]) + + expect: + (path1.equals(path2)) == expectedResult + + where: + scenario | segment1 | segment2 | expectedResult + "same segment" | "MSH" | "MSH" | true + "both null" | null | null | true + "diff segment" | "MSH" | "PID" | false + "null vs string" | null | "MSH" | false + } + + def "hashCode should consider array contents"() { + given: + def path1 = new HL7Path("MSH", [1, 2] as int[]) + def path2 = new HL7Path("MSH", [1, 2] as int[]) + + expect: + path1.hashCode() == path2.hashCode() + } + + def "toString should include array contents"() { + given: + def path = new HL7Path("MSH", [1, 2] as int[]) + + expect: + path.toString() == "HL7Path[segmentName=MSH, indices=[1, 2]]" + } +} diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy index 9869d7634..d55aaa680 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/AssertionRuleEngineTest.groovy @@ -1,7 +1,7 @@ package gov.hhs.cdc.trustedintermediary.rse2e.ruleengine -import ca.uhn.hl7v2.model.Message -import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7Message + +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7Message import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext @@ -85,7 +85,7 @@ class AssertionRuleEngineTest extends Specification { } when: - ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message)) + ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message)) then: 1 * mockLogger.logError(_ as String, exception) @@ -97,7 +97,7 @@ class AssertionRuleEngineTest extends Specification { mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> { throw exception } when: - ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message)) + ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message)) then: 1 * mockLogger.logError(_ as String, exception) @@ -105,7 +105,7 @@ class AssertionRuleEngineTest extends Specification { def "runRules returns nothing when there are no rules"() { when: - def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message)) + def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message)) then: result.isEmpty() @@ -117,7 +117,7 @@ class AssertionRuleEngineTest extends Specification { mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [rule] when: - def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message)) + def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message)) then: result.size() == 1 @@ -130,7 +130,7 @@ class AssertionRuleEngineTest extends Specification { mockRuleLoader.loadRules(_ as InputStream, _ as TypeReference) >> [rule] when: - def result = ruleEngine.runRules(Mock(HapiHL7Message), Mock(HapiHL7Message)) + def result = ruleEngine.runRules(Mock(HL7Message), Mock(HL7Message)) then: result.isEmpty() diff --git a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy index ecd65257d..cccfe8813 100644 --- a/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy +++ b/rs-e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/rse2e/ruleengine/RuleLoaderTest.groovy @@ -4,7 +4,7 @@ import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoader import gov.hhs.cdc.trustedintermediary.ruleengine.RuleLoaderException import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson -import gov.hhs.cdc.trustedintermediary.rse2e.external.hapi.HapiHL7ExpressionEvaluator +import gov.hhs.cdc.trustedintermediary.rse2e.hl7.HL7ExpressionEvaluator import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference import gov.hhs.cdc.trustedintermediary.wrappers.HealthDataExpressionEvaluator @@ -23,7 +23,7 @@ class RuleLoaderTest extends Specification { TestApplicationContext.init() TestApplicationContext.register(RuleLoader, RuleLoader.getInstance()) TestApplicationContext.register(Formatter, Jackson.getInstance()) - TestApplicationContext.register(HealthDataExpressionEvaluator, HapiHL7ExpressionEvaluator.getInstance()) + TestApplicationContext.register(HealthDataExpressionEvaluator, HL7ExpressionEvaluator.getInstance()) TestApplicationContext.injectRegisteredImplementations() tempFile = Files.createTempFile("test_validation_definition", ".json")