HL7 Programming using Java and HAPI - Creating ACK Messages
Introduction
This is part of my HL7 article series. Before we get started on this tutorial, have a quick look at my earlier article titled "A Very Short Introduction to the HL7 2.x Standard". In this tutorial, we will explore how to create ACK (Acknowledgment) messages using Java and the HAPI framework. ACK messages are fundamental to HL7 communication as they confirm receipt and processing status of messages between healthcare systems.
When a HL7 message is sent from one system to another, the receiving system typically responds with an acknowledgment message to indicate whether the message was received successfully, encountered an error during processing, or was rejected entirely. Understanding how to properly create and handle these acknowledgment messages is crucial for building robust healthcare integration solutions.
Tools for Tutorial
- JDK 1.8 SDK or higher
- Eclipse, IntelliJ IDEA, or any other Java IDE (or even a text editor)
- Download HAPI compiled library from the HAPI project website
- View HAPI source code on their GitHub site here
- You can also find all the code demonstrated in this tutorial on GitHub here
“The art of communication is the language of leadership.” ~ James Humes
Understanding HL7 Acknowledgment Codes
Before diving into the code, let us understand the different acknowledgment codes defined by the HL7 standard. These codes indicate the result of processing the original message:
- AA (Application Accept) - Message processed successfully
- AE (Application Error) - Error in message content (syntax or semantic error)
- AR (Application Reject) - Message rejected (application cannot process)
- CA (Commit Accept) - Message committed to safe storage
- CE (Commit Error) - Commit error occurred
- CR (Commit Reject) - Commit rejected
The ACK message structure consists of the following segments:
- MSH - Message Header (mirrors original with response details)
- MSA - Message Acknowledgment (acknowledgment code and reference to original message)
- ERR - Error segment (optional, for error details)
Step 1 of 3 - Define the Acknowledgment Codes
First, let us create an enumeration to represent the different acknowledgment codes we will be using:
package com.saravanansubramanian.hapihl7tutorial.ack;
/**
* Enumeration of HL7 acknowledgment codes.
*/
public enum AcknowledgmentCode {
/** Application Accept - Message processed successfully */
AA("AA"),
/** Application Error - Error in message content */
AE("AE"),
/** Application Reject - Message rejected */
AR("AR"),
/** Commit Accept - Message committed to safe storage */
CA("CA"),
/** Commit Error - Commit error */
CE("CE"),
/** Commit Reject - Commit rejected */
CR("CR");
private final String code;
AcknowledgmentCode(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
Step 2 of 3 - Create the ACK Message Builder
Now let us create a builder class that handles the complex task of creating properly formatted ACK messages. This builder handles swapping sender/receiver information, referencing the original message control ID, setting appropriate acknowledgment codes, and optionally adding error information:
package com.saravanansubramanian.hapihl7tutorial.ack;
import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.v23.message.ACK;
import ca.uhn.hl7v2.model.v23.segment.ERR;
import ca.uhn.hl7v2.model.v23.segment.MSA;
import ca.uhn.hl7v2.model.v23.segment.MSH;
import ca.uhn.hl7v2.util.Terser;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ThreadLocalRandom;
/**
* Builder class for creating ACK (Acknowledgment) messages.
*/
public class AckMessageBuilder {
private static final DateTimeFormatter HL7_TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/**
* Creates an ACK message in response to the original message.
*/
public static ACK createAck(Message originalMessage, AcknowledgmentCode ackCode, String textMessage)
throws HL7Exception, IOException {
ACK ack = new ACK();
// Build MSH segment (response header)
buildMshSegment(ack.getMSH(), originalMessage);
// Build MSA segment (acknowledgment details)
buildMsaSegment(ack.getMSA(), originalMessage, ackCode, textMessage);
return ack;
}
private static void buildMshSegment(MSH msh, Message originalMessage) throws HL7Exception {
// Use Terser for easy access to original message fields
Terser terser = new Terser(originalMessage);
// Standard encoding characters
msh.getFieldSeparator().setValue("|");
msh.getEncodingCharacters().setValue("^~\\&");
// Swap sending and receiving (response goes back to sender)
msh.getSendingApplication().getNamespaceID().setValue(terser.get("/MSH-5-1"));
msh.getSendingFacility().getNamespaceID().setValue(terser.get("/MSH-6-1"));
msh.getReceivingApplication().getNamespaceID().setValue(terser.get("/MSH-3-1"));
msh.getReceivingFacility().getNamespaceID().setValue(terser.get("/MSH-4-1"));
// Set response timestamp
msh.getDateTimeOfMessage().getTimeOfAnEvent().setValue(getCurrentTimestamp());
// Message type is ACK with the same trigger event
msh.getMessageType().getMessageType().setValue("ACK");
String triggerEvent = terser.get("/MSH-9-2");
if (triggerEvent != null && !triggerEvent.isEmpty()) {
msh.getMessageType().getTriggerEvent().setValue(triggerEvent);
}
// Generate unique message control ID for the ACK
msh.getMessageControlID().setValue(generateMessageControlId());
// Copy processing ID and version from original
String processingId = terser.get("/MSH-11-1");
if (processingId != null) {
msh.getProcessingID().getProcessingID().setValue(processingId);
}
String version = terser.get("/MSH-12");
if (version != null) {
msh.getVersionID().setValue(version);
}
}
private static void buildMsaSegment(MSA msa, Message originalMessage,
AcknowledgmentCode ackCode, String textMessage) throws HL7Exception {
Terser terser = new Terser(originalMessage);
// Set acknowledgment code
msa.getAcknowledgementCode().setValue(ackCode.getCode());
// Reference the original message's control ID
String originalControlId = terser.get("/MSH-10");
if (originalControlId != null) {
msa.getMessageControlID().setValue(originalControlId);
}
// Optional text message
if (textMessage != null && !textMessage.isEmpty()) {
msa.getTextMessage().setValue(textMessage);
}
}
private static String getCurrentTimestamp() {
return LocalDateTime.now().format(HL7_TIMESTAMP_FORMAT);
}
private static String generateMessageControlId() {
int randomSuffix = ThreadLocalRandom.current().nextInt(1000, 9999);
return "ACK" + getCurrentTimestamp() + randomSuffix;
}
}
“The single biggest problem in communication is the illusion that it has taken place.” ~ George Bernard Shaw
Step 3 of 3 - Demonstrate ACK Message Generation
Now let us create a demonstration program that shows how to generate different types of ACK messages in response to an incoming HL7 message:
package com.saravanansubramanian.hapihl7tutorial.ack;
import ca.uhn.hl7v2.DefaultHapiContext;
import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.HapiContext;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.v23.message.ACK;
import ca.uhn.hl7v2.parser.Parser;
import java.io.IOException;
public class HapiAckMessageExample {
private static final HapiContext context = new DefaultHapiContext();
// Sample incoming ADT A01 message to acknowledge
private static final String INCOMING_MESSAGE =
"MSH|^~\\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|\r" +
"EVN|A01|20240115120000|||\r" +
"PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||\r" +
"PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||";
public static void main(String[] args) {
System.out.println("=== HAPI ACK Message Generation Example ===\n");
try {
Parser pipeParser = context.getPipeParser();
// Parse the incoming message
System.out.println("1. Incoming message to acknowledge:");
System.out.println(formatForDisplay(INCOMING_MESSAGE));
System.out.println();
Message parsedMessage = pipeParser.parse(INCOMING_MESSAGE);
// Demonstrate different ACK scenarios
demonstrateSuccessfulAck(parsedMessage, pipeParser);
demonstrateErrorAck(parsedMessage, pipeParser);
demonstrateRejectAck(parsedMessage, pipeParser);
// HAPI also provides a built-in way to generate ACKs
demonstrateHapiBuiltInAck(parsedMessage, pipeParser);
System.out.println("\n=== ACK Generation Complete ===");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
try {
context.close();
} catch (IOException e) {
// Ignore cleanup errors
}
}
}
private static void demonstrateSuccessfulAck(Message originalMessage, Parser parser)
throws HL7Exception, IOException {
System.out.println("2. Creating AA (Application Accept) ACK:");
System.out.println(" Use when message was processed successfully.\n");
ACK ack = AckMessageBuilder.createAck(
originalMessage,
AcknowledgmentCode.AA,
"Message processed successfully");
System.out.println(" Generated ACK:");
System.out.println(formatForDisplay(parser.encode(ack)));
System.out.println();
}
private static void demonstrateErrorAck(Message originalMessage, Parser parser)
throws HL7Exception, IOException {
System.out.println("3. Creating AE (Application Error) ACK:");
System.out.println(" Use when there's an error in message content.\n");
ACK ack = AckMessageBuilder.createAck(
originalMessage,
AcknowledgmentCode.AE,
"Patient ID 12345 not found in system");
System.out.println(" Generated ACK:");
System.out.println(formatForDisplay(parser.encode(ack)));
System.out.println();
}
private static void demonstrateRejectAck(Message originalMessage, Parser parser)
throws HL7Exception, IOException {
System.out.println("4. Creating AR (Application Reject) ACK:");
System.out.println(" Use when message is rejected.\n");
ACK ack = AckMessageBuilder.createAck(
originalMessage,
AcknowledgmentCode.AR,
"ADT A01 messages are not accepted by this system");
System.out.println(" Generated ACK:");
System.out.println(formatForDisplay(parser.encode(ack)));
System.out.println();
}
private static void demonstrateHapiBuiltInAck(Message originalMessage, Parser parser)
throws HL7Exception, IOException {
System.out.println("5. Using HAPI's built-in generateACK() method:");
System.out.println(" This is the recommended approach in production.\n");
Message ack = originalMessage.generateACK();
System.out.println(" Generated ACK (using built-in method):");
System.out.println(formatForDisplay(parser.encode(ack)));
}
private static String formatForDisplay(String message) {
return message.replace("\r", "\n ");
}
}
Sample Output
Running the code above will produce output similar to the following:
=== HAPI ACK Message Generation Example ===
1. Incoming message to acknowledge:
MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|
EVN|A01|20240115120000|||
PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||
PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||
2. Creating AA (Application Accept) ACK:
Use when message was processed successfully.
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430221234|P|2.3
MSA|AA|MSG001|Message processed successfully
3. Creating AE (Application Error) ACK:
Use when there's an error in message content.
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430225678|P|2.3
MSA|AE|MSG001|Patient ID 12345 not found in system
4. Creating AR (Application Reject) ACK:
Use when message is rejected.
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430229012|P|2.3
MSA|AR|MSG001|ADT A01 messages are not accepted by this system
5. Using HAPI's built-in generateACK() method:
This is the recommended approach in production.
Generated ACK (using built-in method):
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|12345|P|2.3
MSA|AA|MSG001
Best Practices for ACK Messages
When working with ACK messages in production systems, consider these best practices:
- Always respond - Every incoming message should receive an acknowledgment. Failure to respond can cause the sending system to timeout and potentially resend the message.
- Use HAPI's built-in method - In production code, prefer using
message.generateACK()as it handles many edge cases automatically. - Include meaningful error messages - When returning AE or AR codes, provide descriptive text in the MSA-3 field to help troubleshoot issues.
- Reference the original message - Always include the original message control ID in the MSA-2 field so the sender can correlate the response.
- Handle exceptions gracefully - If an error occurs during message processing, return an AE or AR acknowledgment rather than no response at all.
Conclusion
In this tutorial, we explored how to create HL7 ACK messages using the HAPI framework in Java. We covered the different acknowledgment codes, created a reusable ACK message builder, and demonstrated how to generate various types of acknowledgments. Understanding ACK messages is essential for building reliable healthcare integration systems that properly communicate processing results between applications. This concludes the HL7 Java programming series using HAPI. I hope you found this series helpful in your journey to understanding HL7 programming. Please feel free to explore my other HL7 articles for more information on related topics.