HL7 Programming using Java and HAPI - Handling Binary Data


Introduction

This is part of my HL7 article series. In this tutorial, we will look at how binary data is handled during HL7 message processing. Because HL7 2.x message data is transmitted as text, handling binary data requires additional customization. We will look at some ways in which this can be handled programmatically. 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”. If you want to understand the "nuts and bolts" of the standard from a programming perspective, you may also want to look at my tutorial titled "HL7 Programming using Java". For real-life situations, you will typically employ a library such as "HAPI" to handle your HL7 message processing requirements within your custom Java applications. My previous tutorials on using HAPI illustrated how to utilize this library to create, send, receive, extend as well as parse HL7 messages. In this tutorial, we will use Java and the HAPI library to process binary data for handling non-textual clinical data such as reports, waveform information, etc often required during a number of clinical workflows. Let us get started.

Orders and Observation Reporting in HL7 - Quick Overview

The HL7 2.x standard provides a comprehensive set of message types and triggers to support the process of requesting and receiving order-related messages. An example of an order in a clinical setting is when a doctor requests a pathology lab to analyze a tissue sample and send back a report on a particular patient. Order messages sometimes may not be about a patient at all, and instead may simply be a request from one department to another to request medical supplies. These type of workflows are typically achieved by two or more HL7 interfaces taking on various "application roles" as they are known in the HL7 standard. These application roles include "order placer", "order filler", etc, indicating who is requesting/placing the order and who is actually fulfilling a specific order. The HL7 interfaces involved achieve the overall result by transmitting and responding to a variety of "solicited" and "unsolicited" messages as part of the role play that was described earlier. A "solicited" message is one that is typically received by a HL7 system in response to a query for information that it initiated. On the other hand, an "unsolicited" message is one that is broadcast by a HL7 system to other HL7 systems without the message being explicitly solicited (or asked for) by those receiving systems. In these situations, the receiving systems are typically in a "standby mode" to receive such messages from the senders of these messages. The receiving system may then respond back in one of two ways. It may either respond back with an acknowledgment message (indicating acceptance of these messages) and be done with this communication entirely, or sometimes respond with more than one message where the first message will be acknowledgment of the reception of the message which may then be followed by one or more unsolicited messages which contains additional information such as a when new observations need to reported as they emerge. Please also note that the orders-related workflow can be sometimes be extremely complex and may require many back and forth interactions using HL7 message to achieve the desired result. For instance, scenarios such as changing, or for canceling the original order, etc. are also required in the workflow as well. I will let you explore those workflows on your own. I put a diagram together below that shows an example of message flow during order processing.

Orders and Order Response Workflow

“Poverty is a very complicated issue, but feeding a child isn’t.” ~ Jeff Bridges

Now that we understand a little bit about orders and order responses, let us quickly review the specific HL7 message types and triggers involved. In HL7, the order messages (ORM) are used to signal order requests by the "placer system" to the "filler system". The filler systems may then respond to the placer with observation-related messages (ORU) in either solicited or unsolicited modes. In the solicited mode of response, the filler system merely responds back with any existing observations that match the criteria specified in the original message. However, the receiving systems may also respond back with unsolicited messages when new observations are generated. The observation messages consist of a number of message segments such as MSH, PID, PV1, OBR, OBX, etc. Since we have looked at MSH, PID, PV1 segments already and we know what they are used for, we can focus our attention on the OBR and the OBX segments as they enable the mechanisms through which many of the complex data such as PDF reports, images and waveforms are transmitted as part of the order/observation reporting workflow. The OBR segment serves as a kind of report header. The main purpose of the OBR segment is to help identify the relevant observation set which comprises of unique observations (zero or more) noted during a particular procedure. The attributes or field data of the OBR segment therefore apply to all of the included observation segments (OBX) that follow. The OBR segment may also help confirm the status and completion of the order itself (note that certain domains may use a different and more specialized segment instead of the OBR segment). The OBX segments that follow the OBR segment are used to transmit the unique/individual observations that were captured reported during the procedure that was performed. The OBX segments sometimes may be followed by NTE segments that carry notes and other comments about these observations. OBX segments are sometimes required to carry data such as images, PDF documents, waveforms, etc in addition to textual data. We will spend the rest of the tutorial on how this is achieved.

Tackling Binary Data Transmission using Base-64 Encoding

In my earlier tutorials in this HL7 article series, you will recall having looked at some of the functionality provided by HAPI parsers. I had mentioned there that HAPI parsers assist in the process of encoding a message using which we can convert a message object to pipe delimited or sometimes XML format for transmission over the wire, or for storage to disk or a database. In computer science, an encoder is something that converts some piece of information simply from one format to another. The encoder may take a variety of shapes and forms. It may refer to a piece of software, hardware or may refer to just an algorithm used within a specific software. The encoding itself is performed for many reasons such as for standardization, for compression, or to increase speed of delivery of information, etc. The reverse process of encoding is known as decoding and refers to the process of converting the encoded information back to the original form which may sometimes be done either on a remote system where the information was transmitted to, or even locally (think of what happens when you persist information to a local file or a database for example).

Base-64 Encode/Deccode Operation Overview

Users of any modern operating system such as Linux, Windows or Mac OSX use literally hundreds of encoders in their day to day operations without even being aware of it. Operations on file data alone require the assistance of numerous encoders/decoders behind the scenes. These are needed for the purposes of storing as well as retrieving data such as text files, a variety of binary data files such as Word and PDF documents as well as multimedia files such as images, audio files as well as videos. Sometimes, these encoders are used ingeniously to overcome limitations in some legacy platforms which don't support a certain binary format by taking binary data and converting them to textual form to enable transmission or storage. Base64 is an example of one such encoding mechanism which assists in representing binary data in an ASCII string format by translating it into a radix-64 representation. It is very popular, and is used in many day to day scenarios such as when sending attachments through email, sending binary data through web requests or responses, and also to store complex data in XML format, etc. In HL7, this encoding mechanism is often utilized in order to transmit binary data such as reports, waveforms, images, etc. often needed during order response processing which I touched on earlier.

In ORU messages, binary data often needs to carried in the OBX segment to communicate observation-related information. Base64 comes to our rescue by enabling the encoded data to be carried in the 5th field of an OBX segment (Observation Value). If the data in this field were not encoded correctly, this could cause problems with the overall parsing mechanisms since collisions may arise as a result of potential conflicts with HL7 reserved characters ("|^~\&") which are employed to demarcate segments, fields, components, etc in the HL7 2.x standard. Therefore, data in the binary files are Base64-encoded and then placed directly into the OBX-5 field, and the entire message is then transmitted to the receiving system which then needs to reverse this entire process in order to assemble the transmitted message and its binary payload in its entirety. Let us proceed to look at how a pathology report can be transmitted as part of a ORU order response. Please note that sometimes large binary payloads are split into multiple parts and sent in separate OBX segments. The rules for such customizations are often left entirely to the sites to decide on as there is often more than one way to handle such scenarios in HL7.

Tools and Resources Needed

Example of Sending a Pathology Report in HL7

Before I demonstrate the overall functionality, I will create a small helper class called OurBase64Helper to assist with Base-64 encoding and decoding functionality required during the HL7 message processing. Separating out behaviour like this will enable you to write automated tests, add logging, and troubleshoot problems much more easily. This small class shown here provides wrapper methods around the pre-built .NET functionality to take in input data in the form of a binary file (or simply a string of data) and can either convert it into Base64 format or decode it back to the original format. Each implementation is unique, and you may not require such a helper class as you can easily call the Base64 classes to achieve the desired behavior.

    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.nio.file.Files;
    import org.apache.commons.codec.binary.Base64;

    public class OurBase64Helper {

        public String ConvertToBase64String(File inputFile) throws IOException
        {
            if (!inputFile.exists())
                throw new FileNotFoundException(String.format("The specified input file '%s' does not exist",inputFile.getAbsoluteFile()));

            return new String(Base64.encodeBase64(Files.readAllBytes(inputFile.toPath())));
        }

        public byte[] ConvertFromBase64String(String base64EncodedString) throws BadBase64EncodingException
        {
            if (base64EncodedString == null || base64EncodedString.length() == 0)
                throw new IllegalArgumentException("You must supply byte string for Base64 decoding operation");

            if (base64EncodedString.length() % 4 != 0)
                throw new BadBase64EncodingException("The BASE-64 encoded data is not in correct form (divide by 4 resulted in a remainder)");

            try {
                return Base64.decodeBase64(base64EncodedString);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("Unable to decode Base-64 string supplied for operation. Please check your inputs");
            }
        }
    }

A custom exception class can be used to signal Base64 encoding or decoding errors like this, or you can simply use the IllegalArgumentException class that comes with Java. See the use of the BadBase64EncodingException class in the helper class shown above. Also, note the check using divide by 4. This is to prevent an expensive conversion operation if it can be caught and prevented as early as possible if the transmitted content were not encoded correctly. Some libraries have built-in support to check if a certain byte content is Base-64 encoded. I will let you explore this on your own.

    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    public class BadBase64EncodingException extends Exception   {

        private static final long serialVersionUID = 1L;

        public BadBase64EncodingException(String errorMessage) {
            super(errorMessage);
        }
    }

Now that we have our helper class, let us look at how to construct our order response message (ORU R01) and include the contents of a pathology report in PDF format into an OBX segment that is going to be included in the message. The code illustration below shows a small console program class that uses a message factory to create a ORU R01 message which is then transmitted to a remote system using MLLP over TCP/IP on Port 57550 here. I have already covered the basics of sending HL7 messages in a previous tutorial "HL7 Programming using Java and HAPI - Sending HL7 Messages". So, refer to that article if you want to understand more on how to transmit messages to a remote system using the HAPI HL7 library.

    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    import ca.uhn.hl7v2.DefaultHapiContext;
    import ca.uhn.hl7v2.HapiContext;
    import ca.uhn.hl7v2.app.Connection;
    import ca.uhn.hl7v2.app.Initiator;
    import ca.uhn.hl7v2.model.Message;
    import ca.uhn.hl7v2.parser.Parser;

    public class HapiSendBinaryData {

        private static int PORT_NUMBER = 57550;// change this to whatever your port number is

        // In HAPI, almost all things revolve around a context object
        private static HapiContext context = new DefaultHapiContext();

        public static void main(String[] args) {
            try {
                // create the HL7 message
                // this OruMessageFactory class is not from NHAPI but my own wrapper class
                // check my GitHub page or see my earlier article for reference
                Message oruMessage = OruMessageFactory.CreateMessage();

                // create a new MLLP client over the specified port
                Connection connection = context.newClient("localhost", PORT_NUMBER, false);

                // The initiator which will be used to transmit our message
                Initiator initiator = connection.getInitiator();

                // send the previously created HL7 message over the connection established
                Parser parser = context.getPipeParser();
                System.out.println("Sending message:" + "\n" + parser.encode(oruMessage));
                Message response = initiator.sendAndReceive(oruMessage);

                // display the message response received from the remote party
                String responseString = parser.encode(response);
                System.out.println("Received response:\n" + responseString);

            } catch (Exception e) {
                //In real-life, do something about this exception
                e.printStackTrace();
            }

        }

    }

“Poetry is a form of mathematics, a highly rigorous relationship with words.” ~ Tahar Ben Jelloun

The console program utilizes a message factory called OruMessageFactory shown below to help build the ORU R01 message. This factory class in turn delegates the work of actual message construction to a builder class called OurOruR01MessageBuilder which is shown beneath. Separating the delegation of message construction is very useful when it comes to HL7 message processing particularly when dealing with messages with a large number of message segments. However, I will leave it to you to chose the approach that best fits your needs.

    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    import java.io.IOException;

    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.model.Message;

    public class OruMessageFactory {
        //you will pass in parameters here in the form of a DTO or domain object
        //for message construction in your implementation
        public static Message CreateMessage() throws HL7Exception, IOException
        {
            return new OurOruR01MessageBuilder().Build();
        }
    }
    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    import java.io.File;
    import java.io.IOException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import ca.uhn.hl7v2.HL7Exception;
    import ca.uhn.hl7v2.model.DataTypeException;
    import ca.uhn.hl7v2.model.Varies;
    import ca.uhn.hl7v2.model.v24.datatype.ED;
    import ca.uhn.hl7v2.model.v24.datatype.PL;
    import ca.uhn.hl7v2.model.v24.datatype.XAD;
    import ca.uhn.hl7v2.model.v24.datatype.XCN;
    import ca.uhn.hl7v2.model.v24.datatype.XPN;
    import ca.uhn.hl7v2.model.v24.group.ORU_R01_OBSERVATION;
    import ca.uhn.hl7v2.model.v24.group.ORU_R01_ORDER_OBSERVATION;
    import ca.uhn.hl7v2.model.v24.message.ORU_R01;
    import ca.uhn.hl7v2.model.v24.segment.MSH;
    import ca.uhn.hl7v2.model.v24.segment.OBR;
    import ca.uhn.hl7v2.model.v24.segment.OBX;
    import ca.uhn.hl7v2.model.v24.segment.PID;
    import ca.uhn.hl7v2.model.v24.segment.PV1;

    public class OurOruR01MessageBuilder {
        private ORU_R01 _oruR01Message;
        private OurBase64Helper _ourBase64Helper = new OurBase64Helper();

        /*
        * You can pass in a domain or data transfer object as a parameter when
        * integrating with data from your application here I will leave that to you to
        * explore on your own Using fictional data here for illustration
        */

        public ORU_R01 Build() throws HL7Exception, IOException {
            String currentDateTimeString = getCurrentTimeStamp();
            _oruR01Message = new ORU_R01();
            // you can use the context class's newMessage method to instantiate a message if
            // you want
            _oruR01Message.initQuickstart("ORU", "R01", "P");

            CreateMshSegment(currentDateTimeString);
            CreatePidSegment();
            CreatePv1Segment();
            CreateObrSegment();
            CreateObxSegment();
            return _oruR01Message;
        }

        private void CreateMshSegment(String currentDateTimeString) throws DataTypeException {
            MSH mshSegment = _oruR01Message.getMSH();
            mshSegment.getFieldSeparator().setValue("|");
            mshSegment.getEncodingCharacters().setValue("^~\\&");
            mshSegment.getSendingApplication().getNamespaceID().setValue("Our System");
            mshSegment.getSendingFacility().getNamespaceID().setValue("Our Facility");
            mshSegment.getReceivingApplication().getNamespaceID().setValue("Their Remote System");
            mshSegment.getReceivingFacility().getNamespaceID().setValue("Their Remote Facility");
            mshSegment.getDateTimeOfMessage().getTimeOfAnEvent().setValue(currentDateTimeString);
            mshSegment.getMessageControlID().setValue(getSequenceNumber());
            mshSegment.getVersionID().getVersionID().setValue("2.4");
        }

        private void CreatePidSegment() throws DataTypeException {
            PID pid = _oruR01Message.getPATIENT_RESULT().getPATIENT().getPID();
            XPN patientName = pid.getPatientName(0);
            patientName.getFamilyName().getSurname().setValue("Mouse");
            patientName.getGivenName().setValue("Mickey");
            pid.getPatientIdentifierList(0).getID().setValue("378785433211");
            XAD patientAddress = pid.getPatientAddress(0);
            patientAddress.getStreetAddress().getStreetOrMailingAddress().setValue("123 Main Street");
            patientAddress.getCity().setValue("Lake Buena Vista");
            patientAddress.getStateOrProvince().setValue("FL");
            patientAddress.getCountry().setValue("USA");
        }

        private void CreatePv1Segment() throws DataTypeException {
            PV1 pv1 = _oruR01Message.getPATIENT_RESULT().getPATIENT().getVISIT().getPV1();
            pv1.getPatientClass().setValue("O"); // to represent an 'Outpatient'
            PL assignedPatientLocation = pv1.getAssignedPatientLocation();
            assignedPatientLocation.getFacility().getNamespaceID().setValue("Some Treatment Facility Name");
            assignedPatientLocation.getPointOfCare().setValue("Some Point of Care");
            pv1.getAdmissionType().setValue("ALERT");
            XCN referringDoctor = pv1.getReferringDoctor(0);
            referringDoctor.getIDNumber().setValue("99999999");
            referringDoctor.getFamilyName().getSurname().setValue("Smith");
            referringDoctor.getGivenName().setValue("Jack");
            referringDoctor.getIdentifierTypeCode().setValue("456789");
            pv1.getAdmitDateTime().getTimeOfAnEvent().setValue(getCurrentTimeStamp());
        }

        private void CreateObrSegment() throws DataTypeException {
            ORU_R01_ORDER_OBSERVATION orderObservation = _oruR01Message.getPATIENT_RESULT().getORDER_OBSERVATION();
            OBR obr = orderObservation.getOBR();
            obr.getSetIDOBR().setValue("1");
            obr.getPlacerOrderNumber().getUniversalID().setValue("9434934");
            obr.getFillerOrderNumber().getUniversalID().setValue("123456");
            obr.getUniversalServiceIdentifier().getText().setValue("Document");
            obr.getObservationEndDateTime().getTimeOfAnEvent().setValue(getCurrentTimeStamp());
            ;
            obr.getResultStatus().setValue("F");
        }

        private void CreateObxSegment() throws DataTypeException, IOException {
            ORU_R01_OBSERVATION observation = _oruR01Message.getPATIENT_RESULT().getORDER_OBSERVATION().getOBSERVATION(0);
            OBX obx = observation.getOBX();
            obx.getSetIDOBX().setValue("0");
            obx.getValueType().setValue("ED");
            obx.getObservationIdentifier().getIdentifier().setValue("Report");
            Varies value = obx.getObservationValue(0);
            ED encapsulatedData = new ED(_oruR01Message);
            String base64EncodedStringOfPdfReport = _ourBase64Helper.ConvertToBase64String(new File("C:\\HL7TestInputFiles\\Sample Pathology Lab Report.pdf"));
            encapsulatedData.getEd1_SourceApplication().getHd1_NamespaceID().setValue("Our Java Application");
            encapsulatedData.getTypeOfData().setValue("AP"); //see HL7 table 0191: Type of referenced data
            encapsulatedData.getDataSubtype().setValue("PDF");
            encapsulatedData.getEncoding().setValue("Base64");

            encapsulatedData.getData().setValue(base64EncodedStringOfPdfReport);
            value.setData(encapsulatedData);
        }

        private String getCurrentTimeStamp() {
            return new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        }

        private String getSequenceNumber() {
            String facilityNumberPrefix = "1234"; // some arbitrary prefix for the facility
            return facilityNumberPrefix.concat(getCurrentTimeStamp());
        }
    }

To be able to run this example, I used the HAPI Test Panel software to emulate a remote HL7 system by configuring a listener on the specified port (port 57550 in our example). Our HL7 client was able to successfully create a ORU R01 order response message and transmit this message to the listener. It also received an acknowledgment message from the listener which is then output to the console. I have truncated some of the Base-64 encoded data transmitted in the OBX-5 field for easier illustration here. However, you will notice a much longer message on your machine when you run this program locally.


Sending message:
MSH|^~\&|Our System|Our Facility|Their Remote System|Their Remote Facility|20181006144216||ORU^R01|123420181006144216|P|2.3
PID|378785433211||||Mouse^Mickey||||||123 Main Street^^Lake Buena Vista^FL^^USA
PV1||O|Some Point of Care^^^Some Treatment Facility|ALERT||||99999999^Smith^Jack^^^^^^^^^^456789||||||||||||||||||||||||||||||||||||20181006144216
OBR|||^^123456|^Document||||201810061442|||||||||||||||||F
OBX|0|ED|Report||Our Java Application^AP^PDF^Base64^JVBERi0xLjUNJeLjz9MNCjMxI..(data truncated here for easier display. There will be way more content here)....(truncated for easy display)

Received response:
MSH|^~\&|Their Remote System|Their Remote Facility|Our System|Our Facility|20181006144219.488-0600||ACK^R01|5|P|2.3
MSA|AA|123420181006144216

HAPI Test Panel ORU R01 Message

“Live as if you were to die tomorrow. Learn as if you were to live forever.” ~ Mahatma Gandhi

Example of Receiving a Pathology Report in HL7

Now that we have looked at how to send binary data using a ORU R01 HL7 message, let us look at how to receive and extract the binary data from the transmitted data. This part is relatively easy to do as you simply need to reverse the data processing logic that was done previously. We simply extract the OBX-5 data and run it through the Base-64 decoder and store our PDF file to disk. Sometimes, the name of the PDF file is also transmitted along in the ORU R01 message to enable the receiving system to extract the binary data and save the contents to an output file with same name as specified in the HL7 message.

    package com.saravanansubramanian.hapihl7tutorial.handlingbinarydata;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    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.v24.datatype.ED;
    import ca.uhn.hl7v2.model.v24.group.ORU_R01_OBSERVATION;
    import ca.uhn.hl7v2.model.v24.group.ORU_R01_ORDER_OBSERVATION;
    import ca.uhn.hl7v2.model.v24.message.ORU_R01;
    import ca.uhn.hl7v2.model.v24.segment.OBX;
    import ca.uhn.hl7v2.parser.PipeParser;

    public class HapiExtractBinaryData {

        // this input file has been included in the source code for convenience
        private static String OruR01MessageWithBase64EncodedPdfReportIncluded = "C:\\HL7TestInputFiles\\SaravananOruR01Message.hl7";

        // paths for where the extracted PDF report will be written. Change these as needed
        private static String _extractedPdfOutputDirectory = "C:\\HL7TestOutputs";
        private static String _extractedPdfOutputPath = "C:\\HL7TestOutputs\\ExtractedPdfReport.pdf";

        private static HapiContext context = new DefaultHapiContext();

        public static void main(String[] args) {
            try {
                // instantiate a PipeParser, which handles the "traditional or default encoding"
                PipeParser ourPipeParser = context.getPipeParser();
                // read the message data from file and parse the string format message into a Java message object
                Message hl7Message = ourPipeParser
                        .parse(readFileDataAsString(OruR01MessageWithBase64EncodedPdfReportIncluded));

                // cast to message to an ORU R01 message in order to get access to the message data
                ORU_R01 oruR01Message = (ORU_R01) hl7Message;

                if (oruR01Message != null) {
                    // Display the updated HL7 message using Pipe delimited format
                    System.out.println("Parsed HL7 Message:");
                    System.out.println(ourPipeParser.encode(hl7Message));

                    ExtractPdfDataAndWriteToFile(ourPipeParser, oruR01Message);
                    return;
                }

                throw new IllegalStateException("Unable to access message data. Please check if your input file contains a valid ORU R01 message");

            } catch (Exception e) {
                System.out.println(String.format("Error occured during Order Response PDF extraction operation -> %s",
                        e.getMessage()));
            }
        }

        private static void ExtractPdfDataAndWriteToFile(PipeParser ourPipeParser, ORU_R01 oruR01Message) throws HL7Exception, IOException {

            ED encapsulatedPdfDataInBase64Format = getEncapsulatedDataFromObservationSegment(oruR01Message);

            byte[] extractedPdfByteData = getBase64DecodedPdfByteData(encapsulatedPdfDataInBase64Format);

            File directory = new File(_extractedPdfOutputDirectory);
            if (!directory.exists()) {
                System.out.println(String.format("Creating output directory at '%s'..", _extractedPdfOutputDirectory));
                new File(_extractedPdfOutputDirectory).mkdirs();
            }

            System.out.println(String.format(
                    "Writing the extracted PDF data to '%s'. You should be able to see the decoded PDF content..",
                    _extractedPdfOutputPath));

            writeByteDataToFile(_extractedPdfOutputPath, extractedPdfByteData);

            System.out.println("Extraction operation was successfully completed..");
        }

        private static byte[] getBase64DecodedPdfByteData(ED encapsulatedPdfDataInBase64Format) {
            OurBase64Helper helper = new OurBase64Helper();
            System.out.println("Extracting PDF data stored in Base-64 encoded form from OBX-5..");
            String base64EncodedData = encapsulatedPdfDataInBase64Format.getData().getValue();
            byte[] extractedPdfByteData = helper.ConvertFromBase64String(base64EncodedData);
            return extractedPdfByteData;
        }

        private static ED getEncapsulatedDataFromObservationSegment(ORU_R01 oruR01Message) {
            // start retrieving the OBX segment data to get at the PDF report content
            System.out.println("Extracting message data from parsed message..");
            ORU_R01_ORDER_OBSERVATION ourOrderObservation = oruR01Message.getPATIENT_RESULT().getORDER_OBSERVATION();
            ORU_R01_OBSERVATION observation = ourOrderObservation.getOBSERVATION(0);
            OBX obxSegment = observation.getOBX();
            ED encapsulatedPdfDataInBase64Format = (ED) obxSegment.getObservationValue(0).getData();
            return encapsulatedPdfDataInBase64Format;
        }

        // Note: There are better/easier methods for writing to file in the newer
        // versions of Java. I am using this simply to be backwards compatible with JDK 1.4
        private static void writeByteDataToFile(String destinationFile, byte[] byteData) throws IOException {
            FileOutputStream fileOuputStream = null;

            try {
                fileOuputStream = new FileOutputStream(destinationFile);
                fileOuputStream.write(byteData);
            } catch (IOException e) {
                // simply re-throw the exception for now
                throw e;
            } finally {
                if (fileOuputStream != null) {
                    fileOuputStream.close();
                }
            }
        }

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

    }

The program console output from running our program is shown below. The OBX-5 data here has been truncated for display purposes here. It will be lengthier when you run this locally on your machine.


Parsed HL7 Message:
MSH|^~\&|Our System|Our Facility|Their Remote System|Their Remote Facility|20181006181311||ORU^R01|123420181006181311|P|2.3
PID|378785433211||||Mouse^Mickey||||||123 Main Street^^Lake Buena Vista^FL^^USA
PV1||O|Some Point of Care^^^Some Treatment Facility|ALERT||||99999999^Smith^Jack^^^^^^^^^^456789||||||||||||||||||||||||||||||||||||20181006181311
OBR|||^^123456|^Document||||201810061813|||||||||||||||||F
OBX|0|ED|Report||Our Java Application^AP^PDF^Base64^JVBERi0xLjUNJeLjz9MNCjMxI..(data truncated here for easier display. There will be way more content here)....(truncated for easy display)

Extracting PDF data stored in Base-64 encoded form from OBX-5..
Creating output directory at 'C:\HL7TestOutputs'..
Writing the extracted PDF data to 'C:\HL7TestOutputs\ExtractedPdfReport.pdf'. You should be able to see the decoded PDF content..
Extraction operation was successfully completed..

Extracted PDF Pathology Report

Screen capture above shows the extracted pathology report in PDF format that we had previously encoded into the ORU R01 HL7 message. The entire PDF was successfully reproduced even though it was transformed into textual form during an intermediate step for transmission in a HL7 message using the Base-64 encoding mechanism. You can apply the same process to transmit images, audio or video files. Sometimes, when the input files are very large, the input data is broken into smaller chunks which are then transmitted in multiple OBX segments. Feel free to explore this approach on your own as this is proving to be a lengthy tutorial already.

Please be advised that there is a separate category of HL7 messages called "Medical Document Management (MDM)". These message types are specifically tailored for transmitting information regarding medical documents. The use of these message types is beyond the scope of this tutorial, but I still wanted to bring this to your attention. Also, when needing to transfer binary data, some sites use an approach where a hyperlink of the document is transmitted in the observation value field, and the receiving system uses this link to download the relevant document using that link. Waveform information is another type of information that is communicated in HL7, and it is sometimes transmittted as XML. Because you cannot transfer XML within HL7 2.x, you will want to consider using Base-64 encoding to solve that problem*. HL7 2.x is a flexible standard and permits multiple ways to achieve the same goal. Ultimately, it is left entirely upto to the implementing sites to decide which approach works best for them. Please consult your local HL7 group or an HL7 expert for additional help or assistance on this topic.

Conclusion

That concludes my tutorial on how to handle transmission of binary data within HL7 systems. I used a fictional example involving sending and receiving a pathology report in PDF form for my code examples shown in this tutorial. However, the same approach can be extended to transmit other binary files including images, audio and video files, etc. In the next tutorial in my HL7 article series, I am going to be heading back to finish my .NET articles on HL7 using NHAPI (a .NET port of HAPI) to look at how to parse HL7 messages using that framework. See you then!

* - HL7 3.0 supports XML natively and so you should be able to send XML payload as part of the message payload using CDATA to wrap your waveform content. Please refer to official HL7 documentation for more information