HL7 Programming using Java and HAPI - Parsing HL7 Messages

This is part of my HL7 article series. If you are just getting started with the HL7 2.x standard, please have a quick look at my earlier article titled “A Very Short Introduction to the HL7 2.x Standard”. One of my earlier tutorials in this series titled "HL7 Programming using Java" gave you a foundational understanding of how to build a simple HL7 message processing client and server using the Java programming language alone. We then looked at a Java library called "HAPI" for building larger HL7 applications and explored how to create, transmit and receive messages in my three articles namely "HL7 Programming using HAPI - Creating HL7 Messages", "HL7 Programming using HAPI - Sending HL7 Messages" and "HL7 Programming using HAPI - Receiving HL7 Messages". In this tutorial, we will explore the many ways in which you can extract HL7 message structures such as segment, fields, components, sub-components, etc using the HAPI framework.

Tools for Tutorial

“So many books, so little time.” ~ Frank Zappa

HAPI Parsers - An Overview

HAPI Provides two mechanisms for accessing message information, and these are Parsers and Tersers. They both work quite differently from each other, and in this tutorial, we will look at the parser classes and their capabilities, and what you can use them for in your HL7-enabled applications. In a subsequent tutorial in this series, we will look at tersers as well.

In the field of computer science, the word "parser" often refers to the act of breaking down a piece of text such as a sentence, a string of words or any other linguistic construct into its smaller constituent parts to aid in either the structural or the semantic understanding of that material. This deconstructed information is often displayed in the form of a "parse tree" which refers to the act of displaying this information in a tree like structure. This enables us to see the big picture as to how the various parts are related to one another. The parser classes provided by HAPI contain many useful features including being able to convert from a message string to a message object (this is referred to as "parsing"), and also convert a HL7 message object back to a string format (referred to as "encoding" a message). With these parsers, you can translate the HL7 2.x information from as well as to various formats such as ER7 (sometimes also referred to as "normal encoding") and XML when serialization to a file, a database or transmission across the network is needed. These parsers also enable you to perform message validation and provide support for testing of conformance profiles in HL7. The list of capabilities of HAPI parsers is long. Because there are so many capabilities to cover, I will cover basic parsing operations in this tutorial and cover the more advanced parsing operations such as message validation and conformance profiles in the next tutorial in this series.

When you are dealing with message parsing, something you will begin to really appreciate about the HAPI library is the many strongly typed classes that it provides for dealing with nearly every HL7 message definition laid out in the various HL7 2.x standards. Since there are many versions of the HL7 2.x standard each of which consist of hundreds of message types and trigger events, hand coding parser classes can become extremely time consuming. HAPI authors overcame this problem through a rather ingenious way by auto-generating Java classes based on HL7 standard's official HL7 Microsoft Access Tables containing the HL7 2.x message definitions*. The Java classes generated by this approach help provide convenient binding interfaces that enable application programmers using the HAPI library to access as well as update message data pretty much along the same HL7 abstract message syntax model defined by the HL7 group. So, if you are looking to write code that follows the "HL7 2.x Information Model" more closely, then HAPI parsers can assist you with that. Let us look at some examples of using HAPI parsers now.

Basic Parser Operations

The code below demonstrates a few different operations you can perform using the parser classes. It shows how we can take a HL7 message string (in our case an ACK message response) and parse it into a HL7 message object using the PipeParser. It also shows you how when we have the information in a strongly typed HL7 message object, we can then easily access as well as update any data on the message such as a segment, field, etc. It then shows you how to use the DefaultXmlParser to encode the message object into XML format for display purposes.

    package com.saravanansubramanian.hapihl7tutorial.parsers;

    import ca.uhn.hl7v2.model.Message;
    import ca.uhn.hl7v2.model.v24.message.ACK;
    import ca.uhn.hl7v2.model.v24.segment.MSH;
    import ca.uhn.hl7v2.parser.DefaultXMLParser;
    import ca.uhn.hl7v2.parser.PipeParser;
    import ca.uhn.hl7v2.parser.XMLParser;

    public class HapiParserBasicOperations {

        public static void main(String[] args) {
            
            String messageString = "MSH|^~\\&|SENDING_APPLICATION|SENDING_FACILITY|RECEIVING_APPLICATION|RECEIVING_FACILITY|20110614075841||ACK|1407511|P|2.4||||||\r\n" + 
                    "MSA|AA|1407511|Success||";

            // instantiate a PipeParser, which handles the "traditional or default encoding"
            PipeParser ourPipeParser = new PipeParser();

            try {
                // parse the string format message into a Java message object
                Message hl7Message = ourPipeParser.parse(messageString);

                //cast to ACK message to get access to ACK message data
                if (hl7Message instanceof ACK) {
                    ACK ackResponseMessage = (ACK) hl7Message;
                    
                    //access message data and display it
                    //note that I am using encode method at the end to convert it back to string for display
                    MSH mshSegmentMessageData = ackResponseMessage.getMSH();
                    System.out.println("Message Type is " + mshSegmentMessageData.getMessageType().encode());
                    System.out.println("Message Control Id is " + mshSegmentMessageData.getMessageControlID().encode());
                    System.out.println("Message Timestamp is " + mshSegmentMessageData.getDateTimeOfMessage().encode());
                    System.out.println("Sending Facility is " + mshSegmentMessageData.getSendingFacility().encode() + "\n");
                    
                    //update message data in MSA segment
                    ackResponseMessage.getMSA().getAcknowledgementCode().setValue("AR");
                    
                }

                // instantiate an XML parser
                //HAPI provides 
                XMLParser xmlParser = new DefaultXMLParser();

                // convert from default encoded message into XML format, and send it to standard out for display
                System.out.println(xmlParser.encode(hl7Message));
                
                System.out.println("Printing Message Structure in Abstract Message Syntax Format... ");
                System.out.println(hl7Message.printStructure());
                
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

    }

Running the code above should result in the output similar to what is shown below. We are able to successfully parse the message string into a message object and able to access the message data easily. Here, I am displaying the message type, the message control id, the timestamp of when the message was origination as well as the sending facility for the message. We are also able to encode the message object into XML format for display. We are also able to display the message in an "abstract message syntax format" as seen below.

Message Type is ACK
Message Control Id is 1407511
Message Timestamp is 20110614075841
Sending Facility is SENDING_FACILITY

<?xml version="1.0" encoding="UTF-8"?>
<ACK xmlns="urn:hl7-org:v2xml">
    <MSH>
        <MSH.1>|</MSH.1>
        <MSH.2>^~\&amp;</MSH.2>
        <MSH.3>
            <HD.1>SENDING_APPLICATION</HD.1>
        </MSH.3>
        <MSH.4>
            <HD.1>SENDING_FACILITY</HD.1>
        </MSH.4>
        <MSH.5>
            <HD.1>RECEIVING_APPLICATION</HD.1>
        </MSH.5>
        <MSH.6>
            <HD.1>RECEIVING_FACILITY</HD.1>
        </MSH.6>
        <MSH.7>
            <TS.1>20110614075841</TS.1>
        </MSH.7>
        <MSH.9>
            <MSG.1>ACK</MSG.1>
        </MSH.9>
        <MSH.10>1407511</MSH.10>
        <MSH.11>
            <PT.1>P</PT.1>
        </MSH.11>
        <MSH.12>
            <VID.1>2.4</VID.1>
        </MSH.12>
    </MSH>
    <MSA>
        <MSA.1>AR</MSA.1>
        <MSA.2>1407511</MSA.2>
        <MSA.3>Success</MSA.3>
    </MSA>
</ACK>

Printing Message Structure in Abstract Message Syntax Format... 
ACK (start)
   MSH - MSH|^~\&|SENDING_APPLICATION|SENDING_FACILITY|RECEIVING_APPLICATION|RECEIVING_FACILITY|20110614075841||ACK|1407511|P|2.4
   MSA - MSA|AR|1407511|Success
   [ ERR ] - Not populated
ACK (end)
Parsing of Custom Message Models

An interesting and very useful feature that parsers in HAPI provide is in the area of HL7 message customization. Something you will run into occasionaly when dealing with HL7 2.x messaging systems is what is known as a "Z-segment". Occasionally a requirement emerges where communication of some special type of information is required which is not easily supported by the standard message definitions that are specified in the HL7 standard. In those situations, we can create a custom segment to help transmit this custom data. The standard convention is that all these custom segments begin with the letter Z. For instance, a "ZPV" segment may be used when customized patient visit information needs to be transmitted because there is some specialization information that needs to be recorded as part of the patient visit information. Because custom segments almost always start with the letter Z, they are also referred to as Z-segments. This feature helps provide quite a bit of flexibility to HL7 message communications. However, you need to be able to handle the extraction of the data from Z-segment when this is the case. Writing this parsing logic can be quite tricky as you need to handle the parsing of the entire message and the other message structures contained in it as well. HAPI library provides a flexible approach by allowing you to extend the standard parsing behaviour that you get out of the box by using the CustomModelClassFactory class which can be configured on the HAPI context when using the parser classes. Please see code examples below on how this is done using HAPI using a three-step process.

Step 1 of 3 - Create a Z-segment Class

First, we need to create an new HL7 segment definition that we will store our custom fields. We do this by extending the Abstract Segment class that comes with HAPI that provides a lot of pre-built functionality to manage segment data. This abstract class forces all implementing classes to initialize the various fields contained in them by invoking the init method from the constructor. We then utilize the add method that is already implemented in the abstract this class to define as well as initialize the fields in this segment. In the example below, I am defining two custom fields named "custom notes" and "custom description" that are both of ST data type, but you can implement fields using whatever data type that is supported in the HL7 standard. For the two custom fields, I am also specifying other characteristics such as whether the field is mandatory, the number of repetitions of this field as well as the field length. Lastly, note the method named createNewTypeWithoutReflection at the end of our custom segment class. As the name implies, this method helps indicate whether an instance of a field type should be creatied without using reflection. HAPI documentation suggests that we simply return null, and not to change this unless some very specialized behavior is required. So, reflection is used for message construction in our case.

    
    package com.saravanansubramanian.hapihl7tutorial.parsers.custommodel.v25.segment;

    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.model.AbstractSegment;
    import ca.uhn.hl7v2.model.Group;
    import ca.uhn.hl7v2.model.Type;
    import ca.uhn.hl7v2.model.v25.datatype.ST;
    import ca.uhn.hl7v2.parser.ModelClassFactory;

    public class ZPV extends AbstractSegment {

        private static final long serialVersionUID = 1;
        
        public ZPV(Group parent, ModelClassFactory factory) {
            super(parent, factory);
            init(factory);
        }

        private void init(ModelClassFactory factory) {
            try {
                //initialize the segment with the two custom fields we have need to include
                addFieldToSegment("Custom Location",true,0,100);
                addFieldToSegment("Custom Description",true,0,100);
            } catch (HL7Exception e) {
                log.error("There was an error creating the custom segment ZPV.", e);
            }
        }

        private void addFieldToSegment(String fieldName, boolean isFieldMandatory, int numberOfFieldRepetitions, int fieldLength) throws HL7Exception {
            add(ST.class, isFieldMandatory, numberOfFieldRepetitions, fieldLength, new Object[]{ getMessage() }, fieldName);
        }

        public ST[] getCustomNotes() throws HL7Exception {
            return getTypedStringTextFieldFromFieldIndexPosition(1);
        }
        
        public ST[] getCustomDescription() throws HL7Exception {
            return getTypedStringTextFieldFromFieldIndexPosition(2);
        }

        private ST[] getTypedStringTextFieldFromFieldIndexPosition(int index) {
            return getTypedField(index,new ST[0]);
        }
        
        @Override
        protected Type createNewTypeWithoutReflection(int field) {
            return null;
        }

    }
Step 2 of 3 - Create a Specialized Class by Extending the ADT A01 Message Class

We need to define a new message type to carry the z-segment we just defined earlier as we need to inform the receiving system that this message is not a normal ADT A01 message. Here we are simply extending the ADT A01 message class provided by the HAPI library, and are adding additional behavior to support the new message segment. This approach enables us to build on the behavior already provided by the ADT A01 message class and also the other classes and interfaces that it extends or implements already. As you can see in the code below, we are specifying some additional behavior in the constructor of this new message type class, and are asking it to include new z-segment to its normal payload using the new z-segment class that we previously created.

    package com.saravanansubramanian.hapihl7tutorial.parsers.custommodel.v25.message;

    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.model.v25.message.ADT_A01;
    import ca.uhn.hl7v2.parser.DefaultModelClassFactory;
    import ca.uhn.hl7v2.parser.ModelClassFactory;
    import java.util.Arrays;
    import com.saravanansubramanian.hapihl7tutorial.parsers.custommodel.v25.segment.ZPV;

    public class ZDT_A01 extends ADT_A01 {
        
        // For this example, let's imagine an application that has a custom
        // patient visit segment called ZPV after the PID segment in an ADT^A01 message.
        
        // We will define the ZPV segment to have two custom fields:
        //   1. A non-repeating field (with data type ST) called "Custom Notes"
        //   2 - a non repeating field (also with data type ST) called "Custom Description"

        private static final long serialVersionUID = 1L;

        public ZDT_A01() throws HL7Exception {
            this(new DefaultModelClassFactory());
        }

        public ZDT_A01(ModelClassFactory factory) throws HL7Exception {
            super(factory);

            String[] segmentNamesInMessage = getNames();
            int indexOfPid = Arrays.asList(segmentNamesInMessage).indexOf("PID"); // look for PID segment
            int indexOfZSegment = indexOfPid + 1; // ZPV segment appears immediately after the PID segment

            Class<ZPV> type = ZPV.class;
            boolean required = true;
            boolean repeating = false;

            this.add(type, required, repeating, indexOfZSegment); //add this segment to the message payload
        }

        public ZPV getZPVSegment() throws HL7Exception {
            return getTyped("ZPV", ZPV.class);
        }

    }
Step 3 of 3 - Stitch The Behavior Together

In this last step, we bring all these specialized behavior together to help parse the new message type successfully. The HAPI context class used by the message parser uses a special custom model class factory which looks for our binding classes which we defined earlier at runtime. The package path of where these classes can be located is specified as a parameter. The pipe parser created from this context instance helps parse the custom message based on these binding classes. We can then extract the custom message segment including the custom message data from this message as shown in the code illustration below.

    package com.saravanansubramanian.hapihl7tutorial.parsers;

    import com.saravanansubramanian.hapihl7tutorial.parsers.custommodel.v25.message.ZDT_A01;
    import com.saravanansubramanian.hapihl7tutorial.parsers.custommodel.v25.segment.ZPV;

    import ca.uhn.hl7v2.DefaultHapiContext;
    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.HapiContext;
    import ca.uhn.hl7v2.parser.CustomModelClassFactory;
    import ca.uhn.hl7v2.parser.ModelClassFactory;
    import ca.uhn.hl7v2.parser.Parser;

    public class HapiParserCustomMessageModelExample {

        private static HapiContext context = new DefaultHapiContext();
        
        public static void main(String[] args) throws HL7Exception {

            Parser parser = context.getPipeParser();

            ModelClassFactory ourCustomModelClassFactory = new CustomModelClassFactory("com.saravanansubramanian.hapihl7tutorial.parsers.custommodel");
            context.setModelClassFactory(ourCustomModelClassFactory);

            String messageText = "MSH|^~\\&|IRIS|SANTER|AMB_R|SANTER|200803051508||ZDT^A01|263206|P|2.5\r"
                    + "EVN||200803051509||||200803031508\r"
                    + "PID|||5520255^^^PK^PK~ZZZZZZ83M64Z148R^^^CF^CF~ZZZZZZ83M64Z148R^^^SSN^SSN^^20070103^99991231~^^^^TEAM||ZZZ^ZZZ||19830824|F||||||||||||||||||||||N\r"
                    + "ZPV|Some Custom Notes|Additional custom description of the visit goes here";
            
            //parse this message information into our ZDT custom message
            System.out.println("Attempting to parse custom message into message object...");
            ZDT_A01 zdtA01Message = (ZDT_A01) parser.parse(messageText);
            System.out.println("ZDT^A01 message was parsed successfully");
            
            //extract the ZPV Z-segment from this parsed message
            System.out.println("Retrieving the Z-segment from this message...");
            ZPV zpvSegment = zdtA01Message.getZPVSegment();
            System.out.println("Z-segment ZPV was retrieved successfully...");

            //print the extracted custom fields from the Z-segment below
            System.out.println("Custom Notes retrieved from ZPV segment was -> " + zpvSegment.getCustomNotes()[0].encode()); // Print custom notes
            System.out.println("Custom Description retrieved from ZPV segment was -> " + zpvSegment.getCustomDescription()[0].encode()); // Print custom phone number

        }

    }

The results of running the main program above are shown below. By using an approach such as this, we reduce the amount of code we otherwise need to write and support in our custom HL7 applications. If you are interested in exploring this area further, you should look up the official HAPI source code and/or documentation for more information. While on the topic of Z-segments, I would ask you to be careful around the amount of customization you generally do as this tends to decrease the interoperability and supportability of the overall system especially if the remote systems are upgraded or when new systems come onboard. Besides, the standard already provides support for most scenarios anyway, and so you should always look at the HL7 official documentation before deciding to use Z-segments as there may be ways to do what you are looking for already.

Attempting to parse message string into HL7 message object...
ZDT^A01 message was parsed successfully

Retrieving the Z-segment from this message...
Z-segment ZPV was retrieved successfully...

Custom Notes retrieved from ZPV segment was -> Some Custom Notes
Custom Description retrieved from ZPV segment was -> Additional custom description of the visit goes here

Conclusion

That brings us to the end of yet another tutorial on using the HAPI HL7 library. We looked at some basic message parsing capabilities provided in the HAPI library. The Parser classes provided by this library help us to convert messages from and to various formats such as ER7, XML, etc. We also looked at how parsers enable us to process special Z-segments when site-specific customizations are in place. The parser classes provided by the HAPI library enable application programmers to concentrate more on writing the business logic of their applications, and they help reduce the time required to create and/or extend our HL7 2.x message-enabled applications especially when we need to process many different message types or trigger events. In the next tutorial in my HL7 article series, we will look at tersers quickly before moving on to review the more advanced features provided by the parser classes such as message validation and message conformance profiles. See you then!

* - Always consult the official HAPI and HL7 documentation for latest information as this may change or may have changed already.