diff --git a/prime-router/src/main/kotlin/SettingsProvider.kt b/prime-router/src/main/kotlin/SettingsProvider.kt index ba98f8a168f..7eaa32d3dee 100644 --- a/prime-router/src/main/kotlin/SettingsProvider.kt +++ b/prime-router/src/main/kotlin/SettingsProvider.kt @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonValue import gov.cdc.prime.router.CustomerStatus.ACTIVE import gov.cdc.prime.router.CustomerStatus.INACTIVE import gov.cdc.prime.router.CustomerStatus.TESTING -import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.validation.IItemValidator import gov.cdc.prime.router.validation.MarsOtcElrOnboardingValidator import gov.cdc.prime.router.validation.MarsOtcElrValidator @@ -55,7 +54,6 @@ enum class Topic( val isUniversalPipeline: Boolean = true, val isSendOriginal: Boolean = false, val validator: IItemValidator = NoopItemValidator(), - val hl7ParseConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration? = null, ) { FULL_ELR("full-elr", true, false), ETOR_TI("etor-ti", true, false), diff --git a/prime-router/src/main/kotlin/SubmissionReceiver.kt b/prime-router/src/main/kotlin/SubmissionReceiver.kt index d9cc88a1e5b..fe819504005 100644 --- a/prime-router/src/main/kotlin/SubmissionReceiver.kt +++ b/prime-router/src/main/kotlin/SubmissionReceiver.kt @@ -9,8 +9,8 @@ import gov.cdc.prime.router.azure.ReportWriter import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.fhirengine.engine.FhirConvertQueueMessage -import gov.cdc.prime.router.fhirengine.engine.MessageType import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader /** @@ -268,14 +268,14 @@ class UniversalPipelineReceiver : SubmissionReceiver { when (sender.format) { MimeFormat.HL7 -> { - val messages = HL7Reader(actionLogs).getMessages(content) - val isBatch = HL7Reader(actionLogs).isBatch(content, messages.size) - // create a Report for this incoming HL7 message to use for tracking in the database + val messageCount = HL7MessageHelpers.messageCount(content) + val isBatch = HL7Reader.isBatch(content, messageCount) + // create a Report for this incoming HL7 message to use for tracking in the database report = Report( if (isBatch) MimeFormat.HL7_BATCH else MimeFormat.HL7, sources, - messages.size, + messageCount, metadata = metadata, nextAction = TaskAction.convert, topic = sender.topic, @@ -290,11 +290,8 @@ class UniversalPipelineReceiver : SubmissionReceiver { // actionLogs // ) // } - - // check for valid message type - messages.forEachIndexed { - idx, element -> - MessageType.validateMessageType(element, actionLogs, idx + 1) + if (messageCount == 0 && !actionLogs.hasErrors()) { + actionLogs.error(InvalidReportMessage("Unable to find HL7 messages in provided data.")) } } diff --git a/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt b/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt index c01f4a76fbc..eff8dcd02da 100644 --- a/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt +++ b/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt @@ -5,12 +5,12 @@ import com.google.common.net.HttpHeaders import com.microsoft.azure.functions.HttpRequestMessage import com.microsoft.azure.functions.HttpResponseMessage import com.microsoft.azure.functions.HttpStatus -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Sender import gov.cdc.prime.router.azure.HttpUtilities import gov.cdc.prime.router.azure.HttpUtilities.Companion.isSuccessful import gov.cdc.prime.router.common.JacksonMapperUtilities import gov.cdc.prime.router.fhirengine.translation.hl7.utils.HL7ACKUtils +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.history.DetailedSubmissionHistory import org.apache.logging.log4j.kotlin.Logging @@ -106,12 +106,11 @@ class SubmissionResponseBuilder( contentType == HttpUtilities.hl7V2MediaType && requestBody != null ) { - val hl7Reader = HL7Reader(ActionLogger()) - val messages = hl7Reader.getMessages(requestBody) - val isBatch = hl7Reader.isBatch(requestBody, messages.size) + val messageCount = HL7MessageHelpers.messageCount(requestBody) + val isBatch = HL7Reader.isBatch(requestBody, messageCount) - if (!isBatch && messages.size == 1) { - val message = messages.first() + if (!isBatch && messageCount == 1) { + val message = HL7Reader.parseHL7Message(requestBody) val acceptAcknowledgementType = HL7Reader.getAcceptAcknowledgmentType(message) val ackResponseRequired = acceptAcknowledgmentTypeRespondValues.contains(acceptAcknowledgementType) if (ackResponseRequired) { diff --git a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt index c1a91a09b92..d3ecb48bf78 100644 --- a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt +++ b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt @@ -223,8 +223,8 @@ class ProcessFhirCommands : CliktCommand( (isCli && outputFormat == MimeFormat.HL7.toString()) || ( receiver != null && - (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH) - ) + (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH) + ) ) -> { val (bundle2, inputMessage) = convertHl7ToFhir(contents, receiver) @@ -297,7 +297,7 @@ class ProcessFhirCommands : CliktCommand( } } - private fun evaluateReceiverFilters(receiver: Receiver?, messageOrBundle: MessageOrBundle, isCli: Boolean) { + private fun evaluateReceiverFilters(receiver: Receiver?, messageOrBundle: MessageOrBundle, isCli: Boolean) { if (receiver != null && messageOrBundle.bundle != null) { val reportStreamFilters = mutableListOf>() reportStreamFilters.add(Pair("Jurisdictional Filter", receiver.jurisdictionalFilter)) @@ -522,10 +522,8 @@ class ProcessFhirCommands : CliktCommand( // However, the library used to encode the HL7 message throws an error it there are more than 4 encoding // characters, so this work around exists for that scenario val stringToEncode = hl7String.replace("MSH|^~\\&#|", "MSH|^~\\&|") - val hl7message = HL7Reader.parseHL7Message( - stringToEncode, - null - ) + val hl7message = HL7Reader.parseHL7Message(stringToEncode) + // if a hl7 parsing failure happens, throw error and show the message if (hl7message.toString().lowercase().contains("failed")) { throw CliktError("HL7 parser failure. $hl7message") @@ -534,12 +532,8 @@ class ProcessFhirCommands : CliktCommand( val msh = hl7message.get("MSH") as Segment Terser.set(msh, 2, 0, 1, 1, "^~\\&#") } - val hl7profile = HL7Reader.getMessageProfile(hl7message.toString()) // search hl7 profile map and create translator with config path if found - var fhirMessage = when (val configPath = HL7Reader.profileDirectoryMap[hl7profile]) { - null -> HL7toFhirTranslator(inputSchema).translate(hl7message) - else -> HL7toFhirTranslator(configPath).translate(hl7message) - } + var fhirMessage = HL7toFhirTranslator(inputSchema).translate(hl7message) val stamper = ConditionStamper(LookupTableConditionMapper(Metadata.getInstance())) fhirMessage.getObservations().forEach { observation -> diff --git a/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt b/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt index 1f89270217f..9c23ab1c9ec 100644 --- a/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt +++ b/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt @@ -1,11 +1,11 @@ package gov.cdc.prime.router.cli +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktError import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.file -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.cli.helpers.HL7DiffHelper import gov.cdc.prime.router.fhirengine.utils.HL7Reader @@ -45,9 +45,14 @@ class ProcessHl7Commands : CliktCommand( val comparisonFile = comparisonFile.inputStream().readBytes().toString(Charsets.UTF_8) if (comparisonFile.isBlank()) throw CliktError("File ${this.comparisonFile.absolutePath} is empty.") - val actionLogger = ActionLogger() - val starterMessages = HL7Reader(actionLogger).getMessages(starterFile) - val comparisonMessages = HL7Reader(actionLogger).getMessages(comparisonFile) + val starterMessages = Hl7InputStreamMessageStringIterator(starterFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() + val comparisonMessages = Hl7InputStreamMessageStringIterator(comparisonFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() starterMessages.forEachIndexed { counter, message -> val differences = hl7DiffHelper.diffHl7(message, comparisonMessages[counter]) diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt index 53a4242dfc4..0ce0eb45ab7 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt @@ -50,11 +50,9 @@ import gov.cdc.prime.router.fhirengine.translation.hl7.FhirTransformer import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder -import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.fhirengine.utils.HL7Reader.Companion.parseHL7Message import gov.cdc.prime.router.fhirengine.utils.getObservations import gov.cdc.prime.router.fhirengine.utils.getRSMessageType -import gov.cdc.prime.router.fhirengine.utils.isElr import gov.cdc.prime.router.logging.LogMeasuredTime import gov.cdc.prime.router.report.ReportService import gov.cdc.prime.router.validation.IItemValidator @@ -397,7 +395,7 @@ class FHIRConverter( ) } - FHIREngineRunResult( + FHIREngineRunResult( routeEvent, report, blobInfo.blobUrl, @@ -427,7 +425,7 @@ class FHIRConverter( report, TaskAction.convert, "Submitted report was either empty or could not be parsed into HL7" - ) { + ) { parentReportId(input.reportId) params( mapOf( @@ -479,7 +477,7 @@ class FHIRConverter( "format" to format.name ) ) { - getBundlesFromRawHL7(rawReport, validator, input.topic.hl7ParseConfiguration) + getBundlesFromRawHL7(rawReport, validator) } } catch (ex: ParseFailureError) { actionLogger.error( @@ -571,7 +569,6 @@ class FHIRConverter( private fun getBundlesFromRawHL7( rawReport: String, validator: IItemValidator, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ): List> { val itemStream = Hl7InputStreamMessageStringIterator(rawReport.byteInputStream()).asSequence() @@ -580,17 +577,16 @@ class FHIRConverter( }.toList() return maybeParallelize(itemStream.size, itemStream.stream(), "Generating FHIR bundles in").map { item -> - parseHL7Item(item, hL7MessageParseAndConvertConfiguration) + parseHL7Item(item) }.map { item -> - validateAndConvertHL7Item(item, validator, hL7MessageParseAndConvertConfiguration) + validateAndConvertHL7Item(item, validator) }.collect(Collectors.toList()) } private fun parseHL7Item( item: ProcessedHL7Item, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ) = try { - val message = parseHL7Message(item.rawItem, hL7MessageParseAndConvertConfiguration) + val message = parseHL7Message(item.rawItem) item.updateParsed(message) } catch (e: HL7Exception) { item.updateParsed( @@ -605,20 +601,11 @@ class FHIRConverter( private fun validateAndConvertHL7Item( item: ProcessedHL7Item, validator: IItemValidator, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ): ProcessedHL7Item = if (item.parsedItem != null) { val validationResult = validator.validate(item.parsedItem) if (validationResult.isValid()) { try { - val bundle = when (hL7MessageParseAndConvertConfiguration) { - null -> HL7toFhirTranslator.getHL7ToFhirTranslatorInstance().translate(item.parsedItem) - else -> - HL7toFhirTranslator - .getHL7ToFhirTranslatorInstance( - hL7MessageParseAndConvertConfiguration.hl7toFHIRMappingLocation - ) - .translate(item.parsedItem) - } + val bundle = HL7toFhirTranslator.getHL7ToFhirTranslatorInstance().translate(item.parsedItem) item.setBundle(bundle) } catch (ex: Exception) { item.setConversionError( @@ -759,13 +746,13 @@ class FHIRConverter( * transformer in tests. */ fun getTransformerFromSchema(schemaName: String): FhirTransformer? = if (schemaName.isNotBlank()) { - withLoggingContext(mapOf("schemaName" to schemaName)) { - logger.info("Apply a sender transform to the items in the report") - } - FhirTransformer(schemaName) - } else { - null + withLoggingContext(mapOf("schemaName" to schemaName)) { + logger.info("Apply a sender transform to the items in the report") } + FhirTransformer(schemaName) + } else { + null + } } /** diff --git a/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt b/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt index 8890f198b63..b5c97da7463 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt @@ -1,9 +1,9 @@ package gov.cdc.prime.router.fhirengine.translation +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import com.azure.storage.blob.models.BlobItem import fhirengine.engine.CustomFhirPathFunctions import fhirengine.engine.CustomTranslationFunctions -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration import gov.cdc.prime.router.azure.BlobAccess import gov.cdc.prime.router.fhirengine.config.HL7TranslationConfig @@ -41,7 +41,6 @@ class TranslationSchemaManager : Logging { Regex("/$previousValidBlobName-$timestampRegex") private val previousPreviousValidBlobNameRegex = Regex("/$previousPreviousValidBlobName-$timestampRegex") - private val hL7Reader = HL7Reader(ActionLogger()) /** * Container class that holds the current state for a schema type in a particular azure store. @@ -440,7 +439,11 @@ class TranslationSchemaManager : Logging { ) ).validate( inputBundle, - hL7Reader.getMessages(rawValidationInput.output)[0] + HL7Reader.parseHL7Message( + Hl7InputStreamMessageStringIterator(rawValidationInput.output.byteInputStream()) + .asSequence() + .first() + ), ) } } diff --git a/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt b/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt index 6b05e746c36..cbd8b1b4700 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt @@ -1,6 +1,8 @@ package gov.cdc.prime.router.fhirengine.utils +import ca.uhn.hl7v2.AbstractHL7Exception import ca.uhn.hl7v2.model.v251.datatype.DTM +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration @@ -24,6 +26,8 @@ object HL7MessageHelpers : Logging { */ const val hl7SegmentDelimiter = "\r" + val actionLogger = ActionLogger() + /** * Generate a HL7 Batch file from the list of [hl7RawMsgs] for the given [receiver]. The [hl7RawMsgs] are expected * to be real HL7 messages at this point, so we will not validate their contents here for performance reasons. @@ -34,12 +38,17 @@ object HL7MessageHelpers : Logging { val useBatchHeaders = receiver.translation.useBatchHeaders // Grab the first message to extract some data if not set in the settings val firstMessage = if (hl7RawMsgs.isNotEmpty()) { - val messages = HL7Reader(ActionLogger()).getMessages(hl7RawMsgs[0]) - if (messages.isEmpty()) { + try { + val message = HL7Reader.parseHL7Message(hl7RawMsgs[0]) + Terser(message) + } catch (exception: Hl7InputStreamMessageStringIterator.ParseFailureError) { + logger.warn("Unable to extract batch header values from HL7: ${hl7RawMsgs[0].take(80)} ...") + HL7Reader.logHL7ParseFailure(exception, actionLogger) + null + } catch (exception: AbstractHL7Exception) { logger.warn("Unable to extract batch header values from HL7: ${hl7RawMsgs[0].take(80)} ...") + HL7Reader.recordError(exception, actionLogger) null - } else { - Terser(messages[0]) } } else { null @@ -94,4 +103,8 @@ object HL7MessageHelpers : Logging { return builder.toString() } + + fun messageCount(rawHl7: String): Int { + return Hl7InputStreamMessageStringIterator(rawHl7.byteInputStream()).asSequence().count() + } } \ No newline at end of file diff --git a/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt b/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt index aa48c011d8f..698b41c7697 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt @@ -5,10 +5,8 @@ import ca.uhn.hl7v2.DefaultHapiContext import ca.uhn.hl7v2.ErrorCode import ca.uhn.hl7v2.HL7Exception import ca.uhn.hl7v2.HapiContext -import ca.uhn.hl7v2.model.AbstractMessage import ca.uhn.hl7v2.model.Message import ca.uhn.hl7v2.parser.ParserConfiguration -import ca.uhn.hl7v2.util.Hl7InputStreamMessageIterator import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser import ca.uhn.hl7v2.validation.ValidationException @@ -21,13 +19,10 @@ import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.InvalidReportMessage import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.logging.log4j.Level -import org.apache.logging.log4j.kotlin.Logging +import org.apache.logging.log4j.kotlin.logger import java.util.Date -import ca.uhn.hl7v2.model.v251.message.ORU_R01 as v251_ORU_R01 import ca.uhn.hl7v2.model.v251.segment.MSH as v251_MSH -import ca.uhn.hl7v2.model.v27.message.ORU_R01 as v27_ORU_R01 import ca.uhn.hl7v2.model.v27.segment.MSH as v27_MSH -import fhirengine.translation.hl7.structures.nistelr251.message.ORU_R01 as NIST_ELR_ORU_R01 import fhirengine.translation.hl7.structures.nistelr251.segment.MSH as NIST_MSH private const val MSH_SEGMENT_NAME = "MSH" @@ -35,184 +30,12 @@ private const val MSH_SEGMENT_NAME = "MSH" /** * Converts raw HL7 data (message or batch) to HL7 message objects. */ -class HL7Reader(private val actionLogger: ActionLogger) : Logging { - - /** - * Returns one or more messages read from the raw HL7 data. - * - * This function takes a couple of different approaches to transforming the raw string into messages. - * - * First, it will read the message type from MSH.9 and attempt to find the list of mapped MessageModels. - * See [getMessageModelClasses]. These mappings will typically consist of the v27 structure and the v25 structure - * for that message type. If models are found, the code will iterate over the models and attempt to parse the - * message. If messages are parsed, loop short circuits. - * - * The reason we need to use multiple message models is due to inconsistencies of the specs across different - * organizations. For example, the NIST profile for v251 includes fields that are only available in the v27 - * standard spec. To get around this fact, we take advantage that the specs are mostly backwards compatible; - * a NIST v251 can be parsed using the v271 structure successfully and will now also include the data from the - * fields only available in the standard v27. The only caveat to this approach is that the HAPI library itself - * is not 100% backwards compatible. A common error is that a v251 message will specify a component is a CE, but - * the v27 spec says it must be a CWE; though these two data types are compatible from a field standpoint, the HAPI - * library will throw a type error along the lines of "a CWE field cannot be set to a CE type". To get around this - * issue, if the message cannot be parsed to v27 we fall back to parsing it as a v251 message. - * - * - * If no message models are returned by [getMessageModelClasses], the string is parsed using the default behavior - * of [Hl7InputStreamMessageIterator]. - * - * - * @return one or more HL7 messages - * @throws IllegalArgumentException if the raw data cannot be parsed or no messages were read - */ - fun getMessages(rawMessage: String): List { - val messageModelsToTry = getMessageModelClasses(rawMessage) - val messages: MutableList = mutableListOf() - if (rawMessage.isBlank()) { - actionLogger.error(InvalidReportMessage("Provided raw data is empty.")) - } else if (messageModelsToTry.isEmpty()) { - try { - val iterator = Hl7InputStreamMessageIterator(rawMessage.byteInputStream()) - while (iterator.hasNext()) { - messages.add(iterator.next()) - } - } catch (e: Hl7InputStreamMessageStringIterator.ParseFailureError) { - logHL7ParseFailure(e) - } - } else { - val validationContext = ValidationContextFactory.noValidation() - val parseError = mutableListOf() - run modelLoop@{ - messageModelsToTry.forEach { model -> - val context = DefaultHapiContext(ReportStreamCanonicalModelClassFactory(model)) - context.validationContext = validationContext - try { - val iterator = Hl7InputStreamMessageIterator(rawMessage.byteInputStream(), context) - while (iterator.hasNext()) { - messages.add(iterator.next()) - } - } catch (e: Hl7InputStreamMessageStringIterator.ParseFailureError) { - messages.clear() - parseError.add(e) - } - - if (messages.isNotEmpty()) { - // Don't try other message models if we were able to parse - return@modelLoop - } - } - } - - // if it was able to parse the message through one of the models, then we do not want to log it as an error - val parseLogLevel = if (parseError.size == messageModelsToTry.size) Level.ERROR else Level.WARN - parseError.forEach { currentError -> - logHL7ParseFailure(currentError, messages.isEmpty(), parseLogLevel) - } - } - - if (messages.isEmpty() && !actionLogger.hasErrors()) { - actionLogger.error(InvalidReportMessage("Unable to find HL7 messages in provided data.")) - } - - return messages - } - - /** - * Extracts the message type from the MSH segment and returns the list of message models to use to - * try to parse the messages. - * - * This function assumes all the message types will be the same if this is a HL7 batch. - */ - private fun getMessageModelClasses(rawMessage: String): List> { - try { - val messageProfile = getMessageProfile(rawMessage) - if (messageProfile != null) { - when (messageProfile.typeID) { - "ORU" -> { - return when (messageProfile.profileID) { - // TODO: NIST ELR conformance profile to be enabled in a future PR (rename to "NIST_ELR") - "NIST_ELR_TEST" -> listOf( - NIST_ELR_ORU_R01::class.java - ) - else -> listOf( - v27_ORU_R01::class.java, - v251_ORU_R01::class.java - ) - } - } - else -> { - logger.warn( - "${messageProfile.typeID} did not have any mapped message model classes, " + - "using default behavior" - ) - return emptyList() - } - } - } - } catch (ex: Hl7InputStreamMessageStringIterator.ParseFailureError) { - logHL7ParseFailure(ex) - return emptyList() - } - actionLogger.error(InvalidReportMessage("String did not contain any HL7 messages")) - return emptyList() - } - - /** - * Takes a [rawMessage] and the number of messages [numMessages] in the rawMessage and determines if it is a batch - * or singular HL7 message. It will qualify as a batch message if it follows the HL7 standards and have the Hl7 - * batch headers which start with "FHS" or if they left off the batch headers and just sent multiple messages - */ - fun isBatch(rawMessage: String, numMessages: Int): Boolean { - return rawMessage.startsWith("FHS") || numMessages > 1 - } - - /** - * Takes an [exception] thrown by the HL7 HAPI library, gets the root cause and logs the error into [actionLogger]. - * Sample error messages returned by the HAPI library are: - * Error Code = DATA_TYPE_ERROR-102: 'test' in record 3 is invalid for version 2.5.1 - * Error Code = REQUIRED_FIELD_MISSING-101: Can't find version ID - MSH.12 is null - * This functions only logs messages that contain meaningful data. - * - */ - private fun logHL7ParseFailure( - exception: Hl7InputStreamMessageStringIterator.ParseFailureError, - isError: Boolean = true, - logLevel: Level = Level.ERROR, - ) { - logger.log(logLevel, "Failed to parse message: ${exception.message}") - - // Get the exception root cause and log it accordingly - when (val rootCause = ExceptionUtils.getRootCause(exception)) { - is AbstractHL7Exception -> recordError(rootCause, isError) - else -> throw rootCause - } - } - - private fun recordError(exception: AbstractHL7Exception, isError: Boolean) { - val errorMessage: String = when (exception) { - is ValidationException -> "Validation Failed: ${exception.message}" - - is HL7Exception -> { - when (exception.errorCode) { - ErrorCode.REQUIRED_FIELD_MISSING.code -> "Required field missing: ${exception.message}" - ErrorCode.DATA_TYPE_ERROR.code -> "Data type error: ${exception.message}" - else -> "Failed to parse message" - } - } - - else -> "Failed to parse message" - } - if (isError) { - actionLogger.error(InvalidReportMessage(errorMessage)) - } else { - actionLogger.warn(InvalidReportMessage(errorMessage)) - } - } - +class HL7Reader { companion object { // This regex is used to replace \n with \r while not replacing \r\n val newLineRegex = Regex("(?, - val hl7toFHIRMappingLocation: String, - ) - - /** - * Map of configured message types to their configuration - */ - val messageToConfigMap = mapOf( - HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.10" - ) to HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/HL7/catchall" - ), - HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.11" - ) to HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/HL7/catchall" - ) - ) - - // TODO: https://github.com/CDCgov/prime-reportstream/issues/14116 /** * Accepts a raw HL7 string and uses the MSH segment to detect the [HL7MessageType] which is then used - * to parse the string into an instance of [Message]. If the type is not one that is configured in - * [messageToConfigMap] the default HAPI parsing logic is used + * to parse the string into an instance of [Message]. If the type is not one that is supported in + * [getHL7ParsingContext] the default HAPI parsing logic is used * * @param rawHL7 the HL7 string to convert into a [Message] * - * @return a [Pair] with parsed message and optional type + * @return a [Message] with parsed message and optional type */ fun parseHL7Message( rawHL7: String, - parseConfiguration: HL7MessageParseAndConvertConfiguration?, ): Message { // A carriage return is the official segment delimiter; a newline is not recognized so we replace // them val carriageReturnFixedHL7 = rawHL7.replace(newLineRegex, "\r") val hl7MessageType = getMessageType(carriageReturnFixedHL7) - return getHL7ParsingContext(hl7MessageType, parseConfiguration).pipeParser.parse(carriageReturnFixedHL7) + return getHL7ParsingContext(hl7MessageType).pipeParser.parse(carriageReturnFixedHL7) } /** * Creates a HAPI context that can be used to parse an HL7 string. If no configuration is passed, the function * will return a context with the HAPI defaults which will defer to that library to determine the kind of message * - * @param hl7MessageParseAndConvertConfiguration optional configuration to use when creating a context */ private fun getHL7ParsingContext( hl7MessageType: HL7MessageType?, - hl7MessageParseAndConvertConfiguration: HL7MessageParseAndConvertConfiguration?, ): HapiContext { - return if (hl7MessageParseAndConvertConfiguration == null) { - if (hl7MessageType?.msh93 == "ORU_R01") { + return when (hl7MessageType?.msh93) { + "ORU_R01" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(ORU_R01::class.java), ) - } else if (hl7MessageType?.msh93 == "OML_O21") { + } + "OML_O21" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(OML_O21::class.java), ) - } else if (hl7MessageType?.msh93 == "ORM_O01") { + } + "ORM_O01" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(ORM_O01::class.java), ) - } else { + } + else -> { DefaultHapiContext(ValidationContextFactory.noValidation()) } - } else { - DefaultHapiContext( - ParserConfiguration(), - ValidationContextFactory.noValidation(), - ReportStreamCanonicalModelClassFactory(hl7MessageParseAndConvertConfiguration.messageModelClass), - ) } } @@ -335,7 +117,7 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { */ @Throws(HL7Exception::class) internal fun getMessageType(rawHL7: String): HL7MessageType { - val message = getHL7ParsingContext(null, null) + val message = getHL7ParsingContext(null) .pipeParser // In order to determine the message configuration, only parse the MSH segment since the type of message // is required in order to accurately parse the message in its entirety @@ -349,19 +131,6 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { ) } - // map of HL7 message profiles: maps profile to configuration directory path - val profileDirectoryMap: Map = mapOf( - // TODO: https://github.com/CDCgov/prime-reportstream/issues/14124 - // Pair(MessageProfile("ORU", "NIST_ELR"), "./metadata/HL7/v251-elr"), - ) - - // map of HL7 OIDs to supported conformance profiles - // list of OIDs for NIST ELR retrieved from https://oidref.com/2.16.840.1.113883.9 - private val oidProfileMap: Map = mapOf( - Pair("2.16.840.1.113883.9.10", "NIST_ELR"), - Pair("2.16.840.1.113883.9.11", "NIST_ELR") - ) - // data class to uniquely identify a message profile data class MessageProfile(val typeID: String, val profileID: String) @@ -391,21 +160,6 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { } } - /** - * Get the profile of the [rawmessage] - * If there are multiple HL7 messages the first message's data will be returned - * @param rawmessage string representative of hl7 messages - * @return the message profile, or null if there is no message - */ - fun getMessageProfile(rawmessage: String): MessageProfile? { - val iterator = Hl7InputStreamMessageIterator(rawmessage.byteInputStream()) - if (!iterator.hasNext()) return null - val hl7message = iterator.next() - val msh9 = Terser(hl7message).get("MSH-9") - val profileID = oidProfileMap[Terser(hl7message).get("MSH-21-3")] ?: "" - return MessageProfile(msh9 ?: "", profileID) - } - /** * Get the birthTime from the [message] * @return the birthTime, if available or blank if not @@ -480,5 +234,53 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { else -> null } } + + /** + * Takes a [rawMessage] and the number of messages [numMessages] in the rawMessage and determines if it is a batch + * or singular HL7 message. It will qualify as a batch message if it follows the HL7 standards and have the Hl7 + * batch headers which start with "FHS" or if they left off the batch headers and just sent multiple messages + */ + fun isBatch(rawMessage: String, numMessages: Int): Boolean { + return rawMessage.startsWith("FHS") || numMessages > 1 + } + + /** + * Takes an [exception] thrown by the HL7 HAPI library, gets the root cause and logs the error into [actionLogger]. + * Sample error messages returned by the HAPI library are: + * Error Code = DATA_TYPE_ERROR-102: 'test' in record 3 is invalid for version 2.5.1 + * Error Code = REQUIRED_FIELD_MISSING-101: Can't find version ID - MSH.12 is null + * This functions only logs messages that contain meaningful data. + * + */ + fun logHL7ParseFailure( + exception: Hl7InputStreamMessageStringIterator.ParseFailureError, + actionLogger: ActionLogger, + logLevel: Level = Level.ERROR, + ) { + logger.log(logLevel, "Failed to parse message: ${exception.message}") + + // Get the exception root cause and log it accordingly + when (val rootCause = ExceptionUtils.getRootCause(exception)) { + is AbstractHL7Exception -> recordError(rootCause, actionLogger) + else -> throw rootCause + } + } + + fun recordError(exception: AbstractHL7Exception, actionLogger: ActionLogger) { + val errorMessage: String = when (exception) { + is ValidationException -> "Validation Failed: ${exception.message}" + + is HL7Exception -> { + when (exception.errorCode) { + ErrorCode.REQUIRED_FIELD_MISSING.code -> "Required field missing: ${exception.message}" + ErrorCode.DATA_TYPE_ERROR.code -> "Data type error: ${exception.message}" + else -> "Failed to parse message" + } + } + + else -> "Failed to parse message" + } + actionLogger.error(InvalidReportMessage(errorMessage)) + } } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt b/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt index f17ab0631d3..07e2b485888 100644 --- a/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt +++ b/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt @@ -11,13 +11,12 @@ import ca.uhn.hl7v2.model.Varies import ca.uhn.hl7v2.model.v27.datatype.ID import ca.uhn.hl7v2.model.v27.datatype.NM import ca.uhn.hl7v2.model.v27.datatype.ST -import ca.uhn.hl7v2.model.v27.message.ORU_R01 -import gov.cdc.prime.router.ActionLogger +import fhirengine.translation.hl7.structures.fhirinventory.message.ORU_R01 import gov.cdc.prime.router.fhirengine.utils.HL7Reader import kotlin.test.Test class HL7DiffHelperTests { - private val hL7DiffHelper = HL7DiffHelper() + private val hl7DiffHelper = HL7DiffHelper() private val originalMessage = "MSH|^~\\&#|STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|CDC Atlanta2^" + "11D0668319^CLIA|MEDSS-ELR ^2.16.840.1.114222.4.3.3.6.2.1^ISO|MNDOH^2.16.840.1.114222.4.1.3661^ISO|" + "20230501102531-0400||ORU^R01^ORU_R01|3003786103_4988249_33033|T|2.5.1|||NE|NE|USA||||" + @@ -107,25 +106,21 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `diff hl7`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) assertThat(differences.size).isEqualTo(15) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) assertThat(differences2.size).isEqualTo(15) } @Test fun `test index structure`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val outputNames = outputMessage[0].names + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val outputNames = outputMessage.names val outputMap: MutableMap = mutableMapOf() - hL7DiffHelper.filterNames(outputMessage[0], outputNames, outputMap) + hl7DiffHelper.filterNames(outputMessage, outputNames, outputMap) assertThat(outputMap.size).isEqualTo(9) assertThat( @@ -138,16 +133,14 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type primitive`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputVal = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputVal = ST(inputMessage) inputVal.value = "blah" - val outputVal = ST(outputMessage[0]) + val outputVal = ST(outputMessage) outputVal.value = "blah" - val samePrimitive = hL7DiffHelper.compareHl7Type( + val samePrimitive = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -160,7 +153,7 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- assertThat(samePrimitive).isEmpty() outputVal.value = "test" - val differentPrimitive = hL7DiffHelper.compareHl7Type( + val differentPrimitive = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -175,21 +168,19 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type varies`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputType = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputType = ST(inputMessage) inputType.value = "blah" - val outputType = ST(outputMessage[0]) + val outputType = ST(outputMessage) outputType.value = "blah" - val inputVal = Varies(inputMessage[0]) + val inputVal = Varies(inputMessage) inputVal.data = inputType - val outputVal = Varies(outputMessage[0]) + val outputVal = Varies(outputMessage) outputVal.data = outputType - val sameVaries = hL7DiffHelper.compareHl7Type( + val sameVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -202,7 +193,7 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- assertThat(sameVaries).isEmpty() outputType.value = "test" - val differentVaries = hL7DiffHelper.compareHl7Type( + val differentVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -217,17 +208,15 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type composite`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val id = ID(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val id = ID(inputMessage) id.value = "blah" - val nm = NM(inputMessage[0]) + val nm = NM(inputMessage) nm.value = "blah2" - val sameComposite = hL7DiffHelper.compareHl7Type( + val sameComposite = hl7DiffHelper.compareHl7Type( "", - (inputMessage[0] as ORU_R01).msh.getField(4)[0], - (inputMessage[0] as ORU_R01).msh.getField(4)[0], + (inputMessage as ORU_R01).msh.getField(4)[0], + inputMessage.msh.getField(4)[0], "", 0, 0, @@ -235,11 +224,11 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- ) assertThat(sameComposite).isEmpty() - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val differentComposite = hL7DiffHelper.compareHl7Type( + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val differentComposite = hl7DiffHelper.compareHl7Type( "", - (inputMessage[0] as ORU_R01).msh.getField(4)[0], - (outputMessage[0] as ORU_R01).msh.getField(4)[0], + inputMessage.msh.getField(4)[0], + (outputMessage as ORU_R01).msh.getField(4)[0], "", 0, 0, @@ -250,19 +239,17 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type different types`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputType = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputType = ST(inputMessage) inputType.value = "blah" - val outputType = ST(outputMessage[0]) + val outputType = ST(outputMessage) outputType.value = "blah" - val inputVal = Varies(inputMessage[0]) + val inputVal = Varies(inputMessage) inputVal.data = inputType - val differentVaries = hL7DiffHelper.compareHl7Type( + val differentVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputType, @@ -277,29 +264,25 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `expect no diff messages have blank vs empty MSH 8 (ST), OBR 49 (CWE) respectively`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(msgMSH8OBR49Blank) - val outputMessage = hl7Reader.getMessages(msgMSH8OBR49Empty) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(msgMSH8OBR49Blank) + val outputMessage = HL7Reader.parseHL7Message(msgMSH8OBR49Empty) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) assertThat(differences.size).isEqualTo(0) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) assertThat(differences2.size).isEqualTo(0) } @Test fun `diff output, input missing segments`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) val msg = originalMessage.split("\n").toMutableList() msg.removeAt(1) - val inputMessage = hl7Reader.getMessages(msg.joinToString("\n")) - val outputMessage = hl7Reader.getMessages(originalMessage) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(msg.joinToString("\n")) + val outputMessage = HL7Reader.parseHL7Message(originalMessage) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) // input missing seg SFT assertThat(differences.size).isEqualTo(1) assertThat(differences[0].toString().contains("Input missing segment SFT")) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) // output missing seg SFT assertThat(differences2.size).isEqualTo(1) assertThat(differences[0].toString().contains("Output missing segment SFT")) diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index e23ef1b6e35..3a762bd29ae 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -11,7 +11,6 @@ import ca.uhn.fhir.validation.ResultSeverityEnum import ca.uhn.fhir.validation.SingleValidationMessage import ca.uhn.fhir.validation.ValidationResult import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator -import fhirengine.translation.hl7.structures.nistelr251.message.ORU_R01 import gov.cdc.prime.router.ActionLogDetail import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.CustomerStatus @@ -922,16 +921,6 @@ class FhirConverterTests { fun `should process an HL7 message with a registered profile`() { mockkObject(BlobAccess) mockkObject(HL7Reader.Companion) - every { HL7Reader.Companion.messageToConfigMap } returns mapOf( - HL7Reader.Companion.HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.11" - ) to HL7Reader.Companion.HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/test_fhir_mapping" - ) - ) val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() diff --git a/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt index 533c1386ea2..a7d8e1838a4 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt @@ -6,8 +6,8 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isNull -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.translation.HL7toFhirTranslator +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader import io.github.linuxforhealth.hl7.data.Hl7RelatedGeneralUtils import org.hl7.fhir.r4.model.Bundle @@ -48,16 +48,20 @@ OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by @Test fun `test get message template`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - assertThat(translator.getMessageTemplateType(message[0])).isEqualTo("ORU_R01") + val message = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + assertThat(translator.getMessageTemplateType(message)).isEqualTo("ORU_R01") } @Test fun `test get message model`() { - var message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - val model = translator.getHL7MessageModel(message[0]) + val supportedMessage = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + val model = translator.getHL7MessageModel(supportedMessage) assertThat(model).isNotNull() assertThat(model.messageName).isEqualTo("ORU_R01") @@ -70,17 +74,21 @@ ORC|NW|ORD448811^NIST EHR|||||||20120628070100|||5742200012^Radon^Nicholas^^^^^^ OBR|1|ORD448811^NIST EHR||1000^Hepatitis A B C Panel^99USL|||20120628070100|||||||||5742200012^Radon^Nicholas^^^^^^NPI^L^^^NPI DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 """.trimIndent() - message = HL7Reader(ActionLogger()).getMessages(unsupportedHL7) - assertThat(message.size).isEqualTo(1) - assertFailure { translator.getHL7MessageModel(message[0]) } + val unsupportedMessage = HL7Reader.parseHL7Message(unsupportedHL7) + assertThat( + HL7MessageHelpers.messageCount(unsupportedHL7) + ).isEqualTo(1) + assertFailure { translator.getHL7MessageModel(unsupportedMessage) } } @Test fun `test a quick translation to FHIR`() { // Note that FHIR content will be tested as an integration test - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() @@ -88,9 +96,11 @@ DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 @Test fun `test birth date extension addition`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7ORMWithBirthDateTime) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7ORMWithBirthDateTime) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7ORMWithBirthDateTime) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() @@ -111,9 +121,11 @@ DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 @Test fun `test birth date extension is missing when birthdate is only date`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7ORMWithBirthDate) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7ORMWithBirthDate) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7ORMWithBirthDateTime) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt index a519bcf396e..8fb1d4308ee 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt @@ -2,7 +2,6 @@ package gov.cdc.prime.router.fhirengine.translation.hl7.utils import assertk.assertThat import assertk.assertions.isEqualTo -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.cli.helpers.HL7DiffHelper import gov.cdc.prime.router.fhirengine.utils.HL7Reader import io.mockk.every @@ -19,7 +18,6 @@ import kotlin.test.Test class HL7ACKUtilsTest { inner class Fixture { - val hl7Reader = HL7Reader(ActionLogger()) val hl7DiffHelper = HL7DiffHelper() private val clock = Clock.fixed( @@ -45,17 +43,16 @@ class HL7ACKUtilsTest { val incomingMessage = """ MSH|^~\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE """.trimIndent() - val parsedIncomingMessage = f.hl7Reader.getMessages(incomingMessage).first() + val expectedMessage = """ + MSH|^~\&|ReportStream|CDC|Epic|Hospital|20240921000000+0000||ACK|$id|T|2.5.1|||NE|NE + MSA|CA|4AFA57FE-D41D-4631-9500-286AAAF797E4 + """.trimIndent() + val parsedIncomingMessage = HL7Reader.parseHL7Message(incomingMessage) val ack = f.utils.generateOutgoingACKMessage(parsedIncomingMessage) - val expected = f.hl7Reader.getMessages( - """ - MSH|^~\&|ReportStream|CDC|Epic|Hospital|20240921000000+0000||ACK|$id|T|2.5.1|||NE|NE - MSA|CA|4AFA57FE-D41D-4631-9500-286AAAF797E4 - """ - ).first() - val actual = f.hl7Reader.getMessages(ack).first() + val expected = HL7Reader.parseHL7Message(expectedMessage) + val actual = HL7Reader.parseHL7Message(ack) val diffs = f.hl7DiffHelper.diffHl7(expected, actual) if (diffs.isNotEmpty()) { diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index 5abc630dc42..52fdafd144b 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -16,6 +16,7 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import ca.uhn.fhir.context.FhirContext import ca.uhn.hl7v2.model.v251.segment.MSH +import ca.uhn.hl7v2.util.Hl7InputStreamMessageIterator import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.CodeStringConditionFilter import gov.cdc.prime.router.CustomerStatus @@ -30,7 +31,6 @@ import gov.cdc.prime.router.azure.ConditionStamper import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.LookupTableConditionMapper -import gov.cdc.prime.router.azure.QueueAccess import gov.cdc.prime.router.fhirengine.engine.RSMessageType import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils @@ -77,7 +77,6 @@ class FHIRBundleHelpersTests { val connection = MockConnection(dataProvider) val accessSpy = spyk(DatabaseAccess(connection)) val blobMock = mockkClass(BlobAccess::class) - val queueMock = mockkClass(QueueAccess::class) val metadata = Metadata(schema = Schema(name = "None", topic = Topic.FULL_ELR, elements = emptyList())) private val shorthandLookupTable = emptyMap().toMutableMap() @@ -666,12 +665,11 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - bundle.handleBirthTime(hl7messages[0]) + bundle.handleBirthTime(parsedHl7Message) val patient = FhirPathUtils.evaluate( CustomContext(bundle, bundle), @@ -697,12 +695,11 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - bundle.handleBirthTime(hl7messages[0]) + bundle.handleBirthTime(parsedHl7Message) val patient = FhirPathUtils.evaluate( CustomContext(bundle, bundle), @@ -728,14 +725,13 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is MSH).isTrue() + assertThat(parsedHl7Message["MSH"] is MSH).isTrue() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) val expectedDate = Date(1612994857000) // Wednesday, February 10, 2021 10:07:37 PM GMT assertThat(bundle.timestamp).isEqualTo(expectedDate) @@ -753,14 +749,13 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_2.7.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isTrue() + assertThat(parsedHl7Message["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isTrue() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) val expectedDate = Date(1612994857000) // Wednesday, February 10, 2021 10:07:37 PM GMT assertThat(bundle.timestamp).isEqualTo(expectedDate) @@ -778,15 +773,14 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_2.6.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is MSH).isFalse() - assertThat(hl7Messages[0]["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isFalse() + assertThat(parsedHl7Message["MSH"] is MSH).isFalse() + assertThat(parsedHl7Message["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isFalse() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) assertThat(bundle.timestamp).isNull() assertThat(bundle.identifier.value).isNull() diff --git a/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt index 21e56122939..6f08e4cc95d 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt @@ -7,8 +7,8 @@ import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isTrue import assertk.assertions.startsWith +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Topic @@ -135,7 +135,10 @@ OBX|1|ST|MLI-4000.15^TEMPERATURE||97.7|deg f|||||R|||19980601184619 """.trimIndent() val batchFile = HL7MessageHelpers.batchMessages(listOf(hl7Message, hl7Message), receiver) - val messages = HL7Reader(ActionLogger()).getMessages(batchFile) + val messages = Hl7InputStreamMessageStringIterator(batchFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() assertThat(messages).isNotEmpty() assertThat(messages.size).isEqualTo(2) val a = Terser(messages[0]) diff --git a/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt index aef41a860bf..286074b1230 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt @@ -2,23 +2,17 @@ package gov.cdc.prime.router.fhirengine.utils import assertk.assertThat import assertk.assertions.contains -import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse -import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue +import ca.uhn.hl7v2.ErrorCode +import ca.uhn.hl7v2.HL7Exception import ca.uhn.hl7v2.model.Message -import ca.uhn.hl7v2.model.v27.datatype.CWE -import ca.uhn.hl7v2.model.v27.segment.OBX -import ca.uhn.hl7v2.util.Terser -import gov.cdc.prime.router.ActionLogger -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify -import org.apache.logging.log4j.kotlin.KotlinLogger +import ca.uhn.hl7v2.parser.EncodingNotSupportedException +import org.apache.commons.lang3.exception.ExceptionUtils +import java.lang.Exception import java.text.SimpleDateFormat import kotlin.test.Test @@ -44,36 +38,39 @@ class HL7ReaderTests { @Test fun `test decoding of bad HL7 messages`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - // Empty data val badData1 = "" - hl7Reader.getMessages(badData1) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData1) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } // Some CSV was sent val badData2 = """ a,b,c 1,2,3 """.trimIndent() - hl7Reader.getMessages(badData2) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData2) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } // Some truncated HL7 val badData3 = "MSH|^~\\&#|MEDITECH^2.16.840.1.114222.4.3.2.2.1.321.111^ISO|COCAA^1.2." - hl7Reader.getMessages(badData3) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData3) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } } @Test fun `test decoding of good HL7 messages`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -89,49 +86,12 @@ OBX|6|CWE|82810-3^Pregnant^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|| NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - var messages = hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isFalse() - assertThat(messages.size).isEqualTo(1) - actionLogger.logs.clear() - - val goodData2 = """ -FHS|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|||202102101707-0500 -BHS|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|||202102101707-0500 -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO -SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 -PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N -ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 -OBR|1|73a6e9bd-aaec-418e-813a-0ad33366ca85|0cba76f5-35e0-4a28-803a-2f31308aae9b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F -OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 -OBX|2|CWE|95418-0^Whether patient is employed in a healthcare setting^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|3|CWE|95417-2^First test for condition of interest^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|4|CWE|95421-4^Resides in a congregate care setting^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|5|CWE|95419-8^Has symptoms related to condition of interest^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -SPM|1|0cba76f5-35e0-4a28-803a-2f31308aae9b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|612092|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO -SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 -PID|1||a40ea680-51bc-4a05-bf23-786dd08e64f2^^^Avante at Ormond Beach^PI||Keeling^Tyson^Chuck^^^^L||19550206|M||2106-3^White^HL70005^^^^2.5.1|97065 Mohr Island^Street^North Taylor^TX^69622^^^^48077||7275555555:1:^PRN^^kenton.wilderman@email.com^1^281^2498561|||||||||U^Unknown^HL70189||||||||N -ORC|RE|4ba0f2c4-5f39-4716-9daa-d450573e7019|4ba0f2c4-5f39-4716-9daa-d450573e7019|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 -OBR|1|4ba0f2c4-5f39-4716-9daa-d450573e7019|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F -OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 -OBX|2|CWE|95418-0^Whether patient is employed in a healthcare setting^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|3|CWE|95417-2^First test for condition of interest^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|4|CWE|95421-4^Resides in a congregate care setting^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|5|CWE|95419-8^Has symptoms related to condition of interest^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 -BTS|2 -FTS|1 - """.trimIndent() - messages = hl7Reader.getMessages(goodData2) - assertThat(actionLogger.hasErrors()).isFalse() - assertThat(messages.size).isEqualTo(2) - actionLogger.logs.clear() + val message = HL7Reader.parseHL7Message(goodData1) + assertThat(message.isEmpty).isFalse() } @Test fun `test get message time stamp`() { - val hl7Reader = HL7Reader(ActionLogger()) - fun getTestMessage(timestampStr: String): Message { val rawData = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|$timestampStr||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO @@ -141,9 +101,9 @@ ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85 OBR|1|73a6e9bd-aaec-418e-813a-0ad33366ca85|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 """.trimIndent() - val message = hl7Reader.getMessages(rawData) - assertThat(message.size).isEqualTo(1) - return message[0] + val message = HL7Reader.parseHL7Message(rawData) + assertThat(message.isEmpty).isFalse() + return message } var timestampStr = "" @@ -163,38 +123,32 @@ OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by @Test fun `test decoding of bad HL7 message - MSH Version ID missing`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P||||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO """.trimIndent() - hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isTrue() - assertThat(actionLogger.logs[0].detail.message).contains("Required field missing") - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(goodData1) + } catch (e: HL7Exception) { + assertThat(e.errorCode).isEqualTo(ErrorCode.REQUIRED_FIELD_MISSING.code) + assertThat(ExceptionUtils.getMessage(e)).contains("Can't find version ID - MSH.12 is null") + } } @Test fun `test decoding of bad HL7 message - OBX Wrong data type`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 """.trimIndent() - hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isTrue() - assertThat(actionLogger.logs[0].detail.message).contains("Data type error") - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(goodData1) + } catch (e: HL7Exception) { + assertThat(ExceptionUtils.getMessage(e)).contains("trying to set data type of OBX-5") + } } @Test fun `test isBatch with FSH Header`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ FHS|^~\&|||0.0.0.0.1|0.0.0.0.1|202106221314-0400 BHS|^~\&|||0.0.0.0.1|0.0.0.0.1|202106221314-0400 @@ -214,16 +168,15 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b BTS|2 FTS|1 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isTrue() } @Test fun `test isBatch without FSH Header`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -252,16 +205,15 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isTrue() } @Test fun `test isBatch singular message`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -277,207 +229,101 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isFalse() } @Test fun `test getMessageType`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val justMSH = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO """.trimIndent() - val messages = hL7Reader.getMessages(justMSH) - val type = HL7Reader.getMessageType(messages[0]) + val message = HL7Reader.parseHL7Message(justMSH) + val type = HL7Reader.getMessageType(message) assertThat(type).isEqualTo("ORU") } - @Test - fun `test getMessageProfile`() { - val justMSH = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO - """.trimIndent() - val messages = HL7Reader.getMessageProfile(justMSH) - assertThat(messages).isEqualTo( - HL7Reader.Companion.MessageProfile( - "ORU", - "NIST_ELR" - ) - ) - - val noProfile = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA - """.trimIndent() - val messages2 = HL7Reader.getMessageProfile(noProfile) - assertThat(messages2).isEqualTo( - HL7Reader.Companion.MessageProfile( - "ORU", - "" - ) - ) - } - @Test fun `test getBirthTime_DateTime`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthTimeMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||195808100102034|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthTimeMessage) - val type = HL7Reader.getBirthTime(messages[0]) + val message = HL7Reader.parseHL7Message(birthTimeMessage) + val type = HL7Reader.getBirthTime(message) assertThat(type).isEqualTo("195808100102034") } @Test fun `test getBirthTime_Date`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getBirthTime(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getBirthTime(message) assertThat(type).isEqualTo("19580810") } @Test fun `test getPatientPath_ORM`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT") } @Test fun `test getPatientPath_OML`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||OML^O21^OML_O21|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT") } @Test fun `test getPatientPath_ORU`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT_RESULT/PATIENT") } @Test fun `test getPatientPath_Other`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||TEST^O01^TEST_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo(null) } - @Test - fun `get getMessages no mapped models`() { - val actionLogger = ActionLogger() - val hl7Reader = spyk(HL7Reader(actionLogger), recordPrivateCalls = true) - val mockedLogger = mockk() - - every { hl7Reader.logger } returns mockedLogger - every { mockedLogger.warn(any()) } returns Unit - - val goodData1 = """ - MSH|^~\&|ADT1|GOOD HEALTH HOSPITAL|GHH LAB, INC.|GOOD HEALTH HOSPITAL|198808181126|SECURITY|ADT^A01^ADT_A01|MSG00001|P|2.5.1|| - EVN|A01|200708181123|| - PID|1||PATID1234^5^M11^ADT1^MR^GOOD HEALTH HOSPITAL~123456789^^^USSSA^SS||EVERYMAN^ADAM^A^III||19610615|M||C|2222 HOME STREET^^GREENSBORO^NC^27401-1020|GL|(555) 555-2004|(555)555-2004||S||PATID12345001^2^M10^ADT1^AN^A|444333333|987654^NC| - NK1|1|NUCLEAR^NELDA^W|SPO^SPOUSE||||NK^NEXT OF KIN - PV1|1|I|2000^2012^01||||004777^ATTEND^AARON^A|||SUR||||ADM|A0| - """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - assertThat(messages).hasSize(1) - verify { - mockedLogger.warn("ADT did not have any mapped message model classes, using default behavior") - } - } - - @Test - fun `get getMessages v27 succeeds`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - - val data = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO - SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 - PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N - ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 - OBR|1|sphlspid^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|3015894676_04608646^STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|68991-9^Epidemiologically Important Information^LN^^^^2.69^^^CDC-10516^Poxvirus Serology^L^^2.16.840.1.113883.6.1|||202302101135|||||||||SPHL-000148^CA-Veterans Affairs Palo Alto Healthcare System^^^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO^^^^XX|^NET^Internet^mark.holodniy@va.gov|||||202302131116-0500|||F - OBX|1|CWE|68993-5^Human RNase P RNA XXX Ql NAA+probe^LN^3844^RNaseP human DNA^L^2.69^v_unknown^RNaseP human DNA|ZZYGNASR-1|260385009^Negative^SCT^^^^09012018^^Negative (No human DNA present)||||||C|||202302101135|11D0668319^Centers for Disease Control and Prevention^CLIA^47^Poxvirus Laboratory/Poxvirus and Rabies Branch^L|UIE8@CDC.GOV^Perkins ^Kayla|||20230213102132||||Centers for Disease Control and Prevention^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^11D0668319|1600 Clifton Rd^^Atlanta^GA^30329^USA^B - NTE|1|L|This is a note|RE - SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 - """.trimIndent() - - val messages = hL7Reader.getMessages(data) - assertThat(messages).hasSize(1) - assertThat(actionLogger.hasErrors()).isFalse() - } - - @Test - fun `get getMessages can parse a message that uses the deprecated CE type in OBX2`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - - val data = """ - MSH|^~\&#|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA - SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 - PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N - ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 - OBR|1|sphlspid^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|3015894676_04608646^STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|68991-9^Epidemiologically Important Information^LN^^^^2.69^^^CDC-10516^Poxvirus Serology^L^^2.16.840.1.113883.6.1|||202302101135|||||||||SPHL-000148^CA-Veterans Affairs Palo Alto Healthcare System^^^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO^^^^XX|^NET^Internet^mark.holodniy@va.gov|||||202302131116-0500|||F - OBX|1|CE|68993-5^Human RNase P RNA XXX Ql NAA+probe^LN^3844^RNaseP human DNA^L^2.69^v_unknown^RNaseP human DNA|ZZYGNASR-1|260385009^Negative^SCT^^^^09012018^^Negative (No human DNA present)||||||C|||202302101135|11D0668319^Centers for Disease Control and Prevention^CLIA^47^Poxvirus Laboratory/Poxvirus and Rabies Branch^L|UIE8@CDC.GOV^Perkins ^Kayla|||20230213102132||||Centers for Disease Control and Prevention^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^11D0668319|1600 Clifton Rd^^Atlanta^GA^30329^USA^B - NTE|1|L|This is a note|RE - SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 - """.trimIndent() - - val messages = hL7Reader.getMessages(data) - assertThat(messages).hasSize(1) - assertThat(actionLogger.hasWarnings()).isFalse() - val obxSegment = Terser(messages[0]).getSegment("/PATIENT_RESULT/ORDER_OBSERVATION/OBSERVATION/OBX") as OBX - assertThat(obxSegment.getObservationValue(0).data).isInstanceOf(CWE::class) - } - @Test fun `extract MSH segment values`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - @Suppress("ktlint:standard:max-line-length") - val rawMessage = "MSH|^~\\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE" - val message = hL7Reader.getMessages(rawMessage).first() + val rawMessage = + "MSH|^~\\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE" + val message = HL7Reader.parseHL7Message(rawMessage) assertThat( HL7Reader.getSendingApplication(message) diff --git a/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt b/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt index b7c5e9ce64c..851576300b1 100644 --- a/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt +++ b/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt @@ -3,7 +3,6 @@ package gov.cdc.prime.router.validation import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.utils.HL7Reader import org.junit.jupiter.api.Test @@ -17,8 +16,8 @@ class MarsOtcElrOnboardingValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/fail_onboarding_pass_prod.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isFalse() } @@ -28,8 +27,8 @@ class MarsOtcElrOnboardingValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt b/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt index 16b35f45c91..ee5ceb366a0 100644 --- a/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt +++ b/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt @@ -3,7 +3,6 @@ package gov.cdc.prime.router.validation import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.utils.HL7Reader import org.junit.jupiter.api.Test @@ -17,8 +16,8 @@ class MarsOtcElrValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/sample_2.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isFalse() } @@ -28,8 +27,8 @@ class MarsOtcElrValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } @@ -39,8 +38,8 @@ class MarsOtcElrValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid_altered_msh.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } } \ No newline at end of file diff --git a/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt b/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt index cfca5b5c76a..f18c57cfbdc 100644 --- a/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt +++ b/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt @@ -453,10 +453,7 @@ class TranslationTests { * @return a FHIR bundle as a JSON input stream */ private fun translateToFhir(hl7: String, profile: String? = null): InputStream { - val hl7message = HL7Reader.parseHL7Message( - hl7, - null - ) + val hl7message = HL7Reader.parseHL7Message(hl7) val fhirBundle = if (profile == null) { HL7toFhirTranslator().translate(hl7message) } else {