HL7 Programming using HAPI - Using Tersers

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” for a quick introduction to the HL7 2.x standard if you are just getting started on it. 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 Java alone. We then looked at a Java library called "HAPI" for building larger HL7 applications and explored how to create, transmit, receive as well as parse HL7 messages using this library. In this tutorial, we will look at another way to access HL7 message information using "tersers".

Tools for Tutorial

“The troubleshooting guide contains the answer to every problem except yours” ~ Murphy’s Law

HAPI Tersers - An Overview

If you read my previous tutorial in this series titled “HL7 Programming using HAPI - Parsing Operations”, I explained in it that there were primarily two approaches for accessing as well as updating HL7 message data, namely via "Parsers" and "Tersers". We then reviewed what parsers were at a high level, and later explored some basic operations provided by the various HAPI parser classes in that tutorial. We will now look at a special class called Terser and the features it provides.

Being "terse" means being concise in the English language, and I suppose the authors of this library named these classes that way simply because they enable a concise way to access HL7 message data. In fact, tersers can be way more concise that parsers when retrieving as well as updating information and they use a Xpath-like syntax to refer to locations in the HL7 message. They eliminate the need for using the binding classes and the sometimes deeply nested methods to dig into the data hierarchy of a HL7 message. Tersers come in extremely handy when you interested in retrieving only specific portions of a HL7 message, and you are not particularly interested in rest of the message data during your message processing workflow. Using the code below, I will demonstrate how we can access as well as update message data using tersers.

Some Basic Operations using Tersers

Before I demostrate the terser functionality, I will create a small helper class to wrap the getter and setter behaviors around a terser instance so it is a bit easier to understand the operations in our code examples that follow underneath. You don't need to use a wrapper class like this in your application, but if you find yourself using terser expressions all over your code, streamlining atleast the core data access methods into a single location should make logging and debugging your applications a bit easier.

    package com.saravanansubramanian.hapihl7tutorial.tersers;

    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.util.Terser;

    public class OurTerserHelper {
        
        private Terser _terser;

        public OurTerserHelper(Terser aTerser) {
            if(aTerser == null)
                throw new IllegalArgumentException("Terser object must be passed in for data retrieval operation");
            
            _terser = aTerser;
        }
        
        public String getData(String terserExpression) throws HL7Exception {
            
            if(terserExpression == null || terserExpression.isEmpty())
                throw new IllegalArgumentException("Terser expression must be supplied for data retrieval operation");
            
            return _terser.get(terserExpression);
        }
        
        public void setData(String terserExpression, String value) throws HL7Exception {
            
            if(terserExpression == null || terserExpression.isEmpty())
                throw new IllegalArgumentException("Terser expression must be supplied for set operation");
            
            if(value == null) //we will let an empty string still go through
                throw new IllegalArgumentException("Value for set operation must be supplied");
            
            _terser.set(terserExpression,value);
        }
        
    }

Now that we have our helper class, let us look at some basic operations that use the Terser class. Notice in the code below that I instantiate a terser by wrapping it aorund a message object. Although there are a lot of static methods on this class as well, I rarely use those. However those static methods do provide additional ways of getting at the data especially if you don't like using the terser expressions and like a more statically typed approach for data access. The operations shown below demonstrate various operations such as retrieving field, component and subcomponent-level data from a HL7 message using various terser expressions. Also, please keep the test HL7 file FileWithObservationResultMessage.txt handy as you read through the code below to make sense of what is going on. I have shared this file in my GitHub repository.

    package com.saravanansubramanian.hapihl7tutorial.tersers;

    import java.nio.file.Files;
    import java.nio.file.Paths;
    import ca.uhn.hl7v2.model.Message;
    import ca.uhn.hl7v2.parser.PipeParser;
    import ca.uhn.hl7v2.util.Terser;

    public class HapiTerserBasicOperations {

        public static void main(String[] args) {

            try {

                // see my GitHub page for this file
                String messageString = readHL7MessageFromFileAsString(
                        "C:\\HL7TestInputFiles\\FileWithObservationResultMessage.txt");

                // instantiate a PipeParser, which handles the normal HL7 encoding
                PipeParser ourPipeParser = new PipeParser();

                // parse the message string into a Java message object
                Message orderResultsHl7Message = ourPipeParser.parse(messageString);

                // create a terser object instance by wrapping it around the message object
                Terser terser = new Terser(orderResultsHl7Message);

                // now, let us do various operations on the message
                OurTerserHelper terserHelper = new OurTerserHelper(terser);

                String terserExpression = "MSH-6";
                String dataRetrieved = terserHelper.getData(terserExpression);
                System.out.printf("Field 6 of MSH segment using expression '%s' was: '%s' \n\n", terserExpression,dataRetrieved);

                terserExpression = "/.PID-5-2"; // notice the /. to indicate relative position to root node
                dataRetrieved = terserHelper.getData(terserExpression);
                System.out.printf("Field 5 and Component 2 of the PID segment using expression '%s' was: '%s' \n\n", terserExpression, dataRetrieved);

                terserExpression = "/.*ID-5-2";
                dataRetrieved = terserHelper.getData(terserExpression);
                System.out.printf("Field 5 and Component 2 of the PID segment using wildcard-based expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);

                terserExpression = "/.P?D-5-2";
                dataRetrieved = terserHelper.getData(terserExpression);
                System.out.printf("Field 5 and Component 2 of the PID segment using another wildcard-based expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);

                terserExpression = "/.PV1-9(1)-1"; // note: field repetitions are zero-indexed
                dataRetrieved = terserHelper.getData(terserExpression);
                System.out.printf("2nd repetition of Field 9 and Component 1 for it in the PV1 segment using expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);

            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        public static String readHL7MessageFromFileAsString(String fileName) throws Exception {
            return new String(Files.readAllBytes(Paths.get(fileName)));
        }

    }

The console output from running our program is shown below. The terser expressions shown here should be easy to follow if you are familiar with other languages such X-Path which enable you to navigate through a object hierarchy. Let us look at some more advanced operations using tersers next.

Field 6 of MSH segment using expression 'MSH-6' was: 'ReceivingFac' 

Field 5 and Component 2 of the PID segment using expression '/.PID-5-2' was: 'BEBE' 

Field 5 and Component 2 of the PID segment using wildcard-based expression '/.*ID-5-2' was: 'BEBE' 

Field 5 and Component 2 of the PID segment using another wildcard-based expression '/.P?D-5-2' was: 'BEBE' 

2nd repetition of Field 9 and Component 1 for it in the PV1 segment using expression '/.PV1-9(1)-1' was: '02807'

More Advanced Operations using Tersers

In my opinion, tersers really shine when dealing with deeply nested HL7 messages such as orders and lab results. They are especially useful when needing to deal with HL7 segment groups both for get and set operations on the data in these messages. In HL7, a segment group is a collection of segments that always appear together in sequence. Not all message types contain segment groups. Even when they do, the segment groups can be optional, conditional and/or repeating. Some message types may even have multiple repeating groups of the same segement nested under different segments. See diagram below for an example of a HL7 vaccine update message where I have pointed out repeating OBX segment groups. This makes the process of parsing message data very complex especially if you are writing a custom parser from scratch. However, tersers in HAPI come to our rescue in these situations. They use expression syntaxes that follow the same object model names that define the segment groups in the message definition such as "PATIENT", "ORDER", "OBSERVATION", etc enabling you to traverse the segment hierarchy relatively easily. Tersers also allow us to set data easily.

Example of Segment Groups in HL7 Messages

Let us now look at a code example highligting some of these advanced capabilities. Again, keep the test file FileWithObservationResultMessage.txt that I have uploaded in my GitHub repository handy when you are going through the code below. I would also recommend that you experiment with any other long and complex HL7 message and explore the expression syntaxes even further. The investment will pay off especially if you are going to be dealing with complex HL7 message data.

    package com.saravanansubramanian.hapihl7tutorial.tersers;

    import java.nio.file.Files;
    import java.nio.file.Paths;
    import ca.uhn.hl7v2.model.Message;
    import ca.uhn.hl7v2.parser.PipeParser;
    import ca.uhn.hl7v2.util.Terser;

    public class HapiTerserAdvancedOperations {

        public static void main(String[] args) {
            
            try {
                
                //see my GitHub page for this file
                String messageString = readHL7MessageFromFileAsString("C:\\HL7TestInputFiles\\FileWithObservationResultMessage.txt");
                
                // instantiate a PipeParser, which handles the "traditional or default encoding"
                PipeParser ourPipeParser = new PipeParser();
                            
                // parse the message string into a Java message object
                Message orderResultsHl7Message = ourPipeParser.parse(messageString);
                
                //create a terser object instance by wrapping it around the message object
                Terser terser = new Terser(orderResultsHl7Message);
                
                //now, let us do various operations on the message
                OurTerserHelper terserDemonstrator = new OurTerserHelper(terser);
                
                String terserExpression = "/.OBSERVATION(1)/OBX-3";
                String dataRetrieved = terserDemonstrator.getData(terserExpression);
                System.out.printf("Observation group's 2nd OBX segment's Third field using expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);
                
                terserExpression = "/.OBSERVATION(1)/NTE(1)-3";
                dataRetrieved = terserDemonstrator.getData(terserExpression);
                System.out.printf("Observation group's 2nd NTE segment's Third field using expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);
                
                terserExpression = "/.RESPONSE/ORDER_OBSERVATION/OBSERVATION(0)/OBX(0)-16-2";
                dataRetrieved = terserDemonstrator.getData(terserExpression);
                System.out.printf("Observation group's First OBX segment's 16th Field and its Second component using expression '%s' was: '%s' \n\n",terserExpression, dataRetrieved);
                
                //let us now try a set operation using the terser
                terserExpression = "/.OBSERVATION(1)/NTE-3";
                terserDemonstrator.setData(terserExpression,"This is our override value using the setter");
                System.out.printf("Setting the data for second repetition of the NTE segment and its Third field\n",terserExpression, dataRetrieved);
                
                System.out.println("\nWill display our modified message below \n");
                System.out.println(ourPipeParser.encode(orderResultsHl7Message));
                
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        public static String readHL7MessageFromFileAsString(String fileName)throws Exception
        {
            return new String(Files.readAllBytes(Paths.get(fileName)));
        }
        
    }

The program console output from running our program is shown below. Notice that we have the ability to access any data in a message no matter how deeply it is nested. Tersers expressions are very powerful indeed, and can save us a lot of time and effort when needing to access message data in our HL7 applications.

Observation group's 2nd OBX segment's Third field using expression '/.OBSERVATION(1)/OBX-3' was: 'PT (INR)' 

Observation group's 2nd NTE segment's Third field using expression '/.OBSERVATION(1)/NTE(1)-3' was: 'the range is 2.5 - 3.5.' 

Observation group's First OBX segment 16th Field and Second component using expression '/.RESPONSE/ORDER_OBSERVATION/OBSERVATION(0)/OBX(0)-16-2' was: 'SYSTEMA' 

Setting the data for second repetition of the NTE segment and its Third field

Will display our modified message below 

MSH|^~\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20120226102502||ORU^R01|Q161522306T164850327|P|2.3
PID|1||000168674|000168674|GUNN^BEBE||19821201|F||||||||M|||890-12-3456|||N||||||||N
PV1|1|I||EL|||00976^PHYSICIAN^DAVID^G|976^PHYSICIAN^DAVID^G|01055^PHYSICIAN^RUTH^K~02807^PHYSICIAN^ERIC^LEE~07019^GI^ASSOCIATES~01255^PHYSICIAN^ADAM^I~02084^PHYSICIAN^SAYED~01116^PHYSICIAN^NURUDEEN^A~01434^PHYSICIAN^DONNA^K~02991^PHYSICIAN^NICOLE|MED||||7|||00976^PHYSICIAN^DAVID^G||^^^Chart ID^Vis|||||||||||||||||||||||||20120127204900
ORC|RE|||||||||||00976^PHYSICIAN^DAVID^G
OBR|1|88855701^STDOM|88855701|4083023^PT|||20120226095400|||||||20120226101300|Blood|01255||||000002012057000145||20120226102500||LA|F||1^^^20120226040000^^R~^^^^^R|||||||||20120226040000
OBX|1|NM|PT Patient^PT||22.5|second(s)|11.7-14.9|H|||F|||20120226102500||1^SYSTEMA^SYSTEMB
OBX|2|NM|PT (INR)^INR||1.94||||||F|||20120226102500||1^SYSTEM^SYSTEM
NTE|1||This is our override value using the setter
NTE|2||the range is 2.5 - 3.5.
NTE|3
NTE|4||Studies published in NEJM show that patients treated long-term with low intensity warfarin therapy for prevention of recurrent
NTE|5||venous thromboembolism (with a target INR of 1.5 - 2.0) had a superior outcome.  These results were seen in patients after a median
NTE|6||6 months of full dose anti-coagulation.

Conclusion

That concludes our tutorial on using tersers provided by the HAPI HL7 library. There are a lot of other expressions that are available for you to try out, but hopefully I covered all the important ones that you will frequently use for accessing HL7 message data in your HAPI-enabled HL7 applications. You can review the HAPI documentation as well as source code for additional information. Something I would caution you here is to avoid the temptation to over use these in your application logic especially from a code readability and maintenance perspective. Troubleshooting terser expressions can sometimes be a problem if you are not careful about what you actually intended to do for a specific operation. Unit tests can be extremely useful in these situations since they can enable you to see whether the expression works exactly as you intended for a message being considered for your test. In the next tutorial in my HL7 article series, we will go back to the parser classes and see how they help us towards message validation as well as in testing message conformance profiles. See you then!