generated from CDCgov/template
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
16394 add ack functionality (#16552)
- Loading branch information
Showing
12 changed files
with
575 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package gov.cdc.prime.router.azure.service | ||
|
||
import ca.uhn.hl7v2.model.Message | ||
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.HL7Reader | ||
import gov.cdc.prime.router.history.DetailedSubmissionHistory | ||
import org.apache.logging.log4j.kotlin.Logging | ||
|
||
/** | ||
* Builder class to create either JSON or HL7 response types based on the contents of the | ||
* submitted reports | ||
*/ | ||
class SubmissionResponseBuilder( | ||
private val hL7ACKUtils: HL7ACKUtils = HL7ACKUtils(), | ||
) : Logging { | ||
|
||
/** | ||
* Builds a response to send to the client after submitting a report | ||
* | ||
* This will be an HL7 ACK response given the client has enabled it and requested it. It will otherwise | ||
* default to our default JSON response | ||
*/ | ||
fun buildResponse( | ||
sender: Sender, | ||
responseStatus: HttpStatus, | ||
request: HttpRequestMessage<String?>, | ||
submission: DetailedSubmissionHistory?, | ||
): HttpResponseMessage { | ||
// Azure handles all headers as lowercase | ||
val contentType = request.headers[HttpHeaders.CONTENT_TYPE.lowercase()] | ||
val requestBody = request.body | ||
return when (val responseType = determineResponseType(sender, responseStatus, contentType, requestBody)) { | ||
is HL7ResponseType -> { | ||
logger.info("Returning ACK response") | ||
val responseBody = hL7ACKUtils.generateOutgoingACKMessage(responseType.message) | ||
request.createResponseBuilder(responseStatus) | ||
.header(HttpHeaders.CONTENT_TYPE, HttpUtilities.hl7V2MediaType) | ||
.body(responseBody) | ||
.build() | ||
} | ||
is JsonResponseType -> { | ||
val responseBody = JacksonMapperUtilities | ||
.allowUnknownsMapper | ||
.writeValueAsString(submission) | ||
request | ||
.createResponseBuilder(responseStatus) | ||
.header(HttpHeaders.CONTENT_TYPE, HttpUtilities.jsonMediaType) | ||
.body(responseBody) | ||
.header( | ||
HttpHeaders.LOCATION, | ||
request.uri.resolve( | ||
"/api/waters/report/${submission?.reportId}/history" | ||
).toString() | ||
) | ||
.build() | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Figures out in what format we should respond to a submission with | ||
* | ||
* @return SubmissionResponseType the response type defined in this file | ||
*/ | ||
private fun determineResponseType( | ||
sender: Sender, | ||
responseStatus: HttpStatus, | ||
contentType: String?, | ||
requestBody: String?, | ||
): SubmissionResponseType { | ||
val maybeACKMessage = hl7SuccessResponseRequired(sender, responseStatus, contentType, requestBody) | ||
return when { | ||
maybeACKMessage != null -> HL7ResponseType(maybeACKMessage) | ||
else -> JsonResponseType | ||
} | ||
} | ||
|
||
/** | ||
* This function will return true if the following conditions are met: | ||
* - The sender has the "hl7AcknowledgementEnabled" field set to true | ||
* - The HL7 message has been processed successfully | ||
* - The submitted HL7 contains MSH.15 == "AL" | ||
* - The submitted HL7 is not a batch message | ||
* | ||
* @return HL7 message if ACK required or null otherwise | ||
*/ | ||
private fun hl7SuccessResponseRequired( | ||
sender: Sender, | ||
responseStatus: HttpStatus, | ||
contentType: String?, | ||
requestBody: String?, | ||
): Message? { | ||
val acceptAcknowledgmentTypeRespondValues = setOf("AL") // AL means "Always" | ||
return if ( | ||
sender.hl7AcknowledgementEnabled && | ||
responseStatus.isSuccessful() && | ||
contentType == HttpUtilities.hl7V2MediaType && | ||
requestBody != null | ||
) { | ||
val hl7Reader = HL7Reader(ActionLogger()) | ||
val messages = hl7Reader.getMessages(requestBody) | ||
val isBatch = hl7Reader.isBatch(requestBody, messages.size) | ||
|
||
if (!isBatch && messages.size == 1) { | ||
val message = messages.first() | ||
val acceptAcknowledgementType = HL7Reader.getAcceptAcknowledgmentType(message) | ||
val ackResponseRequired = acceptAcknowledgmentTypeRespondValues.contains(acceptAcknowledgementType) | ||
if (ackResponseRequired) { | ||
message | ||
} else { | ||
null | ||
} | ||
} else { | ||
null | ||
} | ||
} else { | ||
null | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Rather than an enum, we have used a hierarchy that allows an HL7 response type to hold onto the already | ||
* parsed message during our check of MSH.15 to avoid doing that work twice. | ||
*/ | ||
private sealed interface SubmissionResponseType | ||
private data class HL7ResponseType(val message: Message) : SubmissionResponseType | ||
private data object JsonResponseType : SubmissionResponseType |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package gov.cdc.prime.router.fhirengine.translation.hl7.utils | ||
|
||
import ca.uhn.hl7v2.model.Message | ||
import ca.uhn.hl7v2.model.v251.message.ACK | ||
import gov.cdc.prime.router.common.Environment | ||
import gov.cdc.prime.router.fhirengine.utils.HL7Reader | ||
import java.time.Clock | ||
import java.util.Calendar | ||
import java.util.Date | ||
import java.util.TimeZone | ||
import java.util.UUID | ||
|
||
/** | ||
* Helper class to generate HL7 ACK response | ||
*/ | ||
class HL7ACKUtils( | ||
private val clock: Clock = Clock.systemUTC(), | ||
) { | ||
|
||
/** | ||
* Creates the output ACK message according to the spec defined in #16394 | ||
* | ||
* It will read an incoming message and copy some values over to their required locations | ||
* | ||
* It will always output HL7 2.5.1 regardless of the version of the incoming HL7 message | ||
*/ | ||
fun generateOutgoingACKMessage(incomingACKMessage: Message): String { | ||
val outgoingAck = ACK() | ||
|
||
val ackMsh = outgoingAck.msh | ||
ackMsh.msh1_FieldSeparator.value = "|" | ||
ackMsh.msh2_EncodingCharacters.value = "^~\\&" | ||
ackMsh.msh3_SendingApplication.parse("ReportStream") | ||
ackMsh.msh4_SendingFacility.parse("CDC") | ||
ackMsh.msh5_ReceivingApplication.parse(HL7Reader.getSendingApplication(incomingACKMessage)) | ||
ackMsh.msh6_ReceivingFacility.parse(HL7Reader.getSendingFacility(incomingACKMessage)) | ||
ackMsh.msh7_DateTimeOfMessage.time.setValue(getTimestamp()) | ||
ackMsh.msh9_MessageType.parse("ACK") | ||
ackMsh.msh10_MessageControlID.parse(UUID.randomUUID().toString()) | ||
ackMsh.msh11_ProcessingID.parse(if (Environment.isProd()) "P" else "T") | ||
ackMsh.msh12_VersionID.versionID.parse("2.5.1") | ||
ackMsh.msh15_AcceptAcknowledgmentType.parse("NE") | ||
ackMsh.msh16_ApplicationAcknowledgmentType.parse("NE") | ||
|
||
val ackMsa = outgoingAck.msa | ||
ackMsa.msa1_AcknowledgmentCode.parse("CA") | ||
ackMsa.msa2_MessageControlID.parse(HL7Reader.getMessageControlId(incomingACKMessage)) | ||
|
||
return outgoingAck.toString() | ||
} | ||
|
||
/** | ||
* HL7 library requires old Java date libraries, so we do the conversion here. | ||
* | ||
* We must directly specify the UTC timezone or else the HL7 library will use | ||
* your machines local timezone. | ||
*/ | ||
private fun getTimestamp(): Calendar { | ||
val instant = clock.instant() | ||
val date = Date.from(instant) | ||
|
||
val calendar = Calendar.getInstance() | ||
calendar.time = date | ||
calendar.timeZone = TimeZone.getTimeZone("UTC") | ||
|
||
return calendar | ||
} | ||
} |
Oops, something went wrong.