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 brief introduction to the HL7 2.x standard if you are just getting started on 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 Java alone and hopefully helped you understand the building blocks on which message communications occur using this standard. Although one can in theory build a HL7 client or a server application using such an approach, it won't be sufficient when needing to scale the message processing capabilities and support numerous versions of the HL7 2.x standard and the hundreds of message types and trigger events that are part of each HL7 2.x standard. We looked at a Java library called "HAPI" that was built for such a purpose, and then learnt how to create and transmit messages to a remote HL7 listener in my two articles "HL7 Programming using HAPI - Creating HL7 Messages" and "HL7 Programming using HAPI - Sending HL7 Messages". In this tutorial, we will build on these tutorials and look at how we can build a HL7 listener using the HAPI framework that is able to receive and respond to a variety of HL7 messages and trigger types.
Tools for Tutorial
- JDK 1.4 SDK or higher
- Eclipse or any other Java IDE (or even a text editor)
- Download HAPI compiled library or source code from the HAPI website
- Download HAPI Test Panel (optional) from here
- You can also find all the code demonstrated in the tutorial on GitHub here
“Solitude is creativity’s best friend.” ~ Naomi Judd
HL7 Listeners - Some Considerations
In my earlier tutorial "HL7 Programming using Java" you learnt that a socket helps establish a two-way communication link between two programs running on two different (or even same) computers on a network. A socket is always bound to a port number so that the transport layer can help route the data to the correct destination. Socket programming may appear easy especially when using modern languages such as .NET and Java where the frameworks provide a number of high level classes for network-related programming. However, building a mission critical and scalable server application is not trivial and there are many other factors to consider such as message routing, message parsing, error handling, performance and throughput, extensibility, supportability, etc. Just on the area of performance and throughput alone, there are numerous things to consider such as thread and connection pooling and caching, garbage collection of unused objects in memory, etc. These factors and many more have to be considered as part of the overall software design of a well designed HL7 application. Let us look at what HAPI has to offer in this area.
Implementing a Listener using HAPI
In the previous tutorials on HAPI, you saw the use of a special class called HapiContext which came in handy when we wanted to create a HL7 message. Later, we also used this class when we wanted to create a MLLP socket client capable of connecting and communicating with a remote HL7 listener. We used a special utility program called HAPI Test Panel when we needed to simulate a remote listener in order to send messages to. However, we can also use this same class to build a MLLP (over TCP/IP) HL7 listener. HAPI provides numerous other options for building and configuring HL7 listeners as well. It provides an abstract interface called HL7Service based on which a number of pre-built listener classes are provided for you that are capable of listening to multiple connections and multiple message types through specific ports. These listener classes delegate the work of actual processing of any incoming messages to message handlers (known as Applications in HAPI) through the use of special message routing classes (that are aptly named Application Routers). Message routing behaviour for HL7 listeners enabled by these special classes and interfaces can be configured using both code as well as through external configuration files. You can also extend all these classes to implement any custom behaviour in your HL7 system. This loose coupling of behaviour between various aspects of the messaging workflow is one reason why HAPI has been popular and been used under the cover by numerous small as well as large-scale health interface applications over the years. The diagram below that I put together quickly illustrates an example of a messaging architecture that is feasible using the listener classes and message router interfaces provided by the HAPI library.
In this particular tutorial, I want to cover the basics of building HL7 listeners using the HAPI library using a step by step approach similar to one that I had previously used in one of my earlier tutorial called "HL7 Programming using Java". The concepts illustrated in the article should hopefully enable you to explore other advanced options available in this library by yourself. I will mainly use the factory methods available on the HAPIContext class to create a HL7 listener capable of responding to multiple connections through a single port as well through a two-port approach. A two-port listener is a HL7 listener where there is a separate port for inbound traffic and a different port for the outbound message response. HAPI provides many such features through convenient starter classes that you can use or extend, and there are many sensible defaults on most settings for these classes to get started and going easily. However, you can always override the default configurations easily if you wanted. Examples of configurations that you can easily alter include behavior such as timeout settings, exception handling policies, connection pooling, concurrency and thread pool management, etc. Enough talk. Let us dive into some code now.
Step 1 of 5 - A Basic HL7 Listener (without message handling)
The code illustration above shows the implementation of a minimal MLLP listener communicating through the TCP/IP protocol. When you send a ADT message to this server, you will get an Application Internal Error from it as a message response since you don't have any message handlers configured yet (see console output below). We will look at how to configure a message handler in the next step.
Step 2 of 5 - Implementing a Message Handler
Let us now configure a message handler with our MLLP listener. As you see in the code below, the message handler is a class that implements a special interface class called ReceivingApplication. Our message handler (HAPI uses the word Applications to refer to them) simply responds with a positive message acknowledgement regardless of the message version, type or trigger event. I will explain how you can achieve fine grained control over message routing next when we integrate all this together. For now, make note of what a basic message handler implementation looks like below. We will integrate this into our message listener in the next step.
Step 3 of 5 - Listener with Integrated Message Handling and Routing
HL7 listeners are often required to respond to many different HL7 message versions, message types and trigger events. Large health interface engines for example have to communicate and respond to hundreds of messages originating from a variety of hospital systems on the network. To be able to process these messages differently from one another, we usually set up many message handlers each capable of processing a specific trigger event such as ADT^A01 or sometimes a family of messages types. To enable routing behavior so that different message types are routed to various message handlers, HAPI provides classes called Application Routers. Although there are numerous ways to configure routing logic using HAPI, I want to show just a few ways here to get you started easily. You can explore more advanced options on your own once you understand these basics.
Running the code above should result in a different message response than we got previously when we did not have a message handler. The HL7 listener parsed the incoming message, and based on the message type that was recognized, it then routed this message to the OurSimpleApplication application class that we created for message processing and message acknowledgment. Program console output as a result of running the code above is shown below.
“Together we can change the world, just one random act of kindness at a time.” ~ Ron Hall
Step 4 of 5 - Exception Handling and Connection Listeners
Although we would ideally want our systems to all work seamlessly with each other all the time, issues arise with both the sending and the receiving systems from time to time, and these issues need to be addressed quickly. This is especially important in healthcare environments when information exchange needs to happen in an expedient manner. To enable these systems and personnel operating these system in these environments to diagnose and rectify these issues quickly a HL7 listener should be fitted with good exception handling capabilities as well as with solid error logging and alerting capabilities. HAPI framework provides plenty of support in these aspects by providing several configuration options and "hooks" to manage exception-related processing during the message flow using primarily three different ways:
- By using a special HL7 exception class
- By using application exception policies
- By using exception handlers that can be configured for connections
I will illustrate some of these exception handling capabilities through the use of some simple examples below. First we will look at what HAPI provides by default. In the example below, I simulate an exception condition by using a dummy application that simply throws an error. The HAPI framework can trap errors like these and send a message acknowledgement with the error message contained in the exception. This behaviour "comes out of the box" for you. In addition to error handling, sometimes it is also useful to capture information regarding connection establishment required for any diagnostic troubleshooting process. You can inject this behaviour into your HAPI-enabled HL7 applications using connection listeners. In the example below, I am also registering a connection listener with our listener so we can listen for connections that are opened and closed from time to time.
As you saw in the above example, HAPI provides a default error handling mechanism even when you don't handle any exceptions explicitly. This however will not be sufficient in many implementations as you will want to perform additional error logging, alerting and customization of the message responses for the listener. This is where exception handlers prove extremely useful. When configured on a listener (see setExceptionHandler method on the HL7 listener) you can gain greater visibility and control over the exceptions that are occuring across the various applications/message handlers registered on the listener, and you can perform addtional processing as necessary. These exception handlers receive information such as the incoming message that caused the exception to happen, the meta data of this incoming message to assist with any additional routing without having to parse the raw message explicitly, the default outgoing NAK (NAK stands for "negative acknowledgment message") as well as the exception itself. When using exception handlers ensure that something is always returned from the processException method, or else the client will not receive any response to the original message it transmitted to the listener. Not responding to a message is not considered a good practice as most HL7 systems will not continue to transmit any additional messages until then delaying potentially hundreds of messages that need to be transmitted sequentially. The code below shows an exception handler class that can be injected into this listener.
Step 5 of 5 - Building a Two-Port Listening Service
In this last step of this tutorial, I want to cover another feature that HAPI provides. This feature enables application programmers to develop two-port message interfaces relatively easily although you still have deal with sockets directly. These two-port listeners are needed sometimes when the inbound and outbound messages need to be communicated through separate ports. A simple two-port listener is shown below along with the console output when running such a program.
For those of you wondering what a DefaultExecutorService (shown in the last line of code illustration above), it is a concurrency feature offered by the Java language to enable you to run tasks asynchronously without having to manage thread management ourselves (it manages the thread pooling and caching internally for you). HAPI provides a default executor service as part of the Default HAPI Context we used in our examples when managing connection listeners, and this implementation should be suffient for most small to mid-size messaging requirements. However, HAPI also allows you to inject your own custom executor service if you wanted full control over the thread pooling strategy. Normally an instance of an Executor Service (default or otherwise) will not be automatically destroyed when all tasks assigned to it have been completed, and it will still linger around and wait for any new tasks to be assigned. However, if you have no use for it, and you want to clean up properly after your completion of your application, you will want to explicitly shut down the ExecutorService using either the shutdown and shutdownNow methods. I will let you read the official Java documentation and also consult the official HAPI documentation if you are keen to explore this area.
That brings us to the end of this tutorial on building HL7 listeners using the HAPI framework. It was a bit lengthy but hopefully it provided a good overview of all the essentials when starting with HAPI and HL7 listeners. A topic that I could not delve deeply in this tutorial as much as I would have to liked to is on message acknowledgments. This is an extremely topic in the overall scheme of things especially when you are building HL7 listeners. This also requires a lengthy explanation of the many ways of responding to HL7 messages. However, I will at least mention here that there are several message acknowledgment modes specified in the HL7 standard. These modes are a way of specifying whether you want a response saying a message was simply received by the listener, or whether you want to know whether a message was semantically understood and fully processed by the listener in a deeper sense. There are also non-HL7 static string responses that are sometimes used when a message response in a HL7 format is not possible for some reason by the receiving application. Like I said previously, this is a very large topic and you should consult the official documentation for more information. I will hopefully at least cover some of this information in a future tutorial. However, in the next tutorial in my HL7 article series, I will cover message parsing functionality available within the HAPI framework which will enable you to extract content from the HL7 messages easily. See you then!