HL7 Messaging

HL7 v2 Messaging

This chapter describes how IPF facilitates HL7 v2 messaging.


HL7 v2 Messaging Overview

HL7's Version 2.x messaging standard is the workhorse of electronic data exchange in the clinical domain and arguably the most widely implemented standard for healthcare in the world. There have been seven releases of the Version 2.x Standard to date.

The HL7 Standard covers messages that exchange information in the general areas of Patient Demographics, Patient Charges and Accounting, Clinical Observations, Medical Records Document Management, and many more.

HL7 Version 2.6 represents HL7's latest development efforts to the line of Version 2 Standards that date back to 1989.

Features

IPF's HL7 v2 support does not reinvent the wheel. It leverages HAPI (http://hl7api.sourceforge.net), one of the most proven HL7 v2 Java libraries. It provides, however, features on top of HAPI that adds a lot of convenience compared to the original API, and retrofits some missing items.

Feature Functionality See
HL7v2 DSL A domain specific language based on the Groovy programming language for manipulating HL7 messages. HL7 message processing in IPF applications becomes almost trivial. HL7 DSL
Extended HL7 Parser classes and Factories For more flexibility in defining valid sets of HL7 structures Extensions to HAPI
Convenient creation of HL7 messages API for creating new messages and responses, in particular HL7 acknowledgements. Extensions to HAPI
HL7 v2 Validation API A specialized DSL dedicated to defining validation rules for HL7 messages HL7 Message Validation
Camel adapters Camel data types for using the HL7 v2 DSL and Validation inside Camel integration routes. Camel DSL Extensions for HL7

Configuring HL7 v2 Messaging

This section explains how to configure IPF's HL7 messaging support.

Make sure you already have correctly set up

  • Maven 2.0.9 or better
  • a possibly empty IPF project

After configuration you will be able to use the HL7 v2 related DSLs in a standalone scenario.

  1. Add the necessary dependencies to your project's Maven 2 descriptor.
    pom.xml for standalone scenario
    ...
    <!-- Dependency for accessing and manipulating HL7 v2 structures -->
    <dependency>
        <groupId>org.openehealth.ipf.modules</groupId>
        <artifactId>modules-hl7dsl</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    
    <!-- Dependency for extending the API of the HAPI HL7 library -->
    <dependency>
        <groupId>org.openehealth.ipf.modules</groupId>
        <artifactId>modules-hl7</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    ...
    


  2. Depending on the HL7 v2 versions, add the corresponding dependencies to the HAPI library. HAPI supports versions 2.2 through 2.6.
    pom.xml
    ...
    <!-- Dependency for HL7 v2.5 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v25</artifactId>
        <version>0.6</version>
    </dependency>
    
    <!-- Dependency for HL7 v2.5.1 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v251</artifactId>
        <version>0.6</version>
    </dependency>
    ...
    


  3. Register the HL7 extensions in the Spring Application Context of your application
    context.xml
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:lang="http://www.springframework.org/schema/lang"
           xmlns:camel="http://camel.apache.org/schema/spring"
           xsi:schemaLocation="
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/lang 
    http://www.springframework.org/schema/lang/spring-lang-2.5.xsd
    http://camel.apache.org/schema/spring
    http://camel.apache.org/schema/spring/camel-spring.xsd">
    
        <!-- Setting up the Camel context -->
    
        <camel:camelContext id="camelContext">
           <camel:routeBuilder ref="routeBuilder" />
        </camel:camelContext>
    
        <bean id="routeBuilder" depends-on="routeModelExtender" class="..." />
    
        ...
    
        <!-- HAPI extensions -->
        <bean id="hapiModelExtension" 
            class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
            <property name="mappingService" ref="..." />
        </bean>
    
        <!-- Register the extensions -->
        <bean id="routeModelExtender"
            class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
            <property name="routeModelExtensions">
                <list>
                    ...
                    <ref bean="hapiModelExtension" />
                </list>
            </property>
        </bean>
    
        ...
    
    </beans>
    

HL7 v2 DSL

This section gives a detailed introduction to the Groovy-based HL7 v2 domain specific language.

The HL7 v2 DSL provides a unique programming interface for handling HL7 messages. Its API aligns very closely with natural language and the syntax of HL7 v2 as often seen in specifications and requirements. You don't need to translate anymore from the language of the "HL7 world" into the language of the "developer's world".
The DSL can be subdivided into the following groups of functionality:

  • Construction: copying or loading messages from file or a plain string
  • Navigation: accessing HL7 v2 substructures like groups, segments, or fields
  • Manipulation: assigning new values to HL7 structures
  • Rendering: writing a message or parts thereof to their external representation

For the purpose of demonstrating the DSL, a ORU_R01 message of HL7 v2.5 is taken as example.

Construction

Use load to construct a message from an HL7 file on the classpath or from an InputStream.

import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.*

def message = load('oru-r01-25.hl7')

Alternatively, the message can be created from a string representation of a message directly:

import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.*

def messageString = ... // an HL7 message string
def message = make(messageString)

You can easily create a message as a copy of an existing message:

def messageCopy = message.copy()
Anonymous types

Groovy doesn't require to specify the exact type of a variable, instead you can use the def keyword. For HL7 v2 processing, this is a very convenient feature that saves you many explicit type checks and type casts.

The message object that is constructed is a org.openehealth.ipf.modules.hl7dsl.MessageAdapter object that wraps the original HAPI message object. The complete HL7 v2 DSL only works with MessageAdapter objects. If you have a native HAPI message object, you can wrap it manually:

ca.uhn.hl7v2.model.Message hapiMessage = ....
MessageAdapter message = new MessageAdapter(hapiMessage)

Navigation

The DSL offers a position-based navigation of HL7 structures and fields. It's all valid Groovy Syntax, accomplished by operator overloading and metaclass programming, so you don't need a intermediate step that parses the expressions (cf. the Terser class in HAPI.)

Navigation to groups and segments

Groups and Segments can be accessed by name like an object property.

def msh   = message.MSH               // Obtain the Message Header Segment

def group = message.PATIENT_RESULT(0) // Obtain the first PATIENT_RESULT group
def pid   = group.PATIENT.PID         // Obtain the PID segment contained inside the PATIENT group.            

Note that although the HL7 DSL hides much of the technical details and APIs, you still require profound knowledge of the HL7 specifications when working with HL7 messages. In the example above, e.g. you need to know that for ORU_R01 structures the PID segment is nested inside two groups.

Navigation to fields

Obtaining fields is similar to obtaining structures except that fields are often referred to by number instead of by name. Fields are accessed like an array field; components in a composite field are accessed like a two-dimension array:

def composite = message.MSH[3]     // MSH-3 = sending application composite field
def primitive = message.MSH[3][2]  // MSH-3-2 = universal ID primitive field

It's also possible to navigate by specifying the field names instead of the number.

def primitive = message.MSH.sendingApplication.universalIDType

Take care, however, that along with the change of internal message structures, individual field names change between HL7 versions although they refer to the same position of the field in a segment. If you don't know the version of the HL7 message in advance, better use the more concise index notation. Example:

def messageType = message.MSH.messageType.messageType // only works for HL7 v2.2 and 2.3 messages
messageType = message.MSH.messageType.messageCode     // only works for HL7 v2.4\+ messages
messageType = message.MSH[9][1]                       // works for all HL7 versions

Field values

Field variables render to their string encoding e.g. when printed, by implementing an appropriate toString() method. However, for literal comparison or variable assignment you
use the value property to obtain the value of a primitive field.

String primitiveValue = message.MSH[3][2].value
primitiveValue = message.MSH[3][2].toString()
if ("xyz".equals(message.MSH[3][2].value)) {
   ...
}

HL7 Null Values

The HL7 DSL treats explicit HL7 null values (two double quotes "", cf. HL7 2.5, Final, Section 2.5.3) in a special way.

  • value will convert "" into an empty string
  • originalValue returns the double quotes
  • isNullValue() returns true, if the original value of the field was "".

Therefore, if PID[11](0)[1][1] (first Street or Mailing Address) was "", the following assertions are true:

assertEquals ''  , pid[11](0)[1][1].value
assertEquals '""', pid[11](0)[1][1].originalValue
assertTrue pid[11](0)[1][1].isNullValue()

Repetitions

Groups, Segments and Fields may repeat. Use parentheses like with regular method calls in order to obtain a certain element of a repeating structure. The next example shows how to navigate in a nested repetitive structure.

def group = message.PATIENT_RESULT(0).PATIENT  // access first PATIENT_RESULT group
def nk1   = group.NK1(0)                       // access first NK1 segment
def phone = nk1[5](0)                          // access first NK1-5 field (phone)

To get a list of elements of a repeating structure, simply omit the index so that it looks like a method call without parameters.

def phones = nk1[5]()  // returns a list of phone elements

Furthermore, repetitions can be counted:

def count = nk1.count(5)      // returns size of the phone number list

Smart navigation

Navigating HL7 messages as described above usually requires knowledge about the specified message structure, which is often not visible by looking at the printed message:

def nk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0) // Repetitions of groups or segments:
def phoneNumber = nk1[5](0)[1]                     // Repetition of fields
def familyName  = nk1[2][1][1]                     // Name is first component of FN composite type

To make things worse, the internal structure changes between HL7 versions. In higher versions, primitive fields are sometimes replaced with composite fields, having the so far used primitive as first component. This appears to be backwards compatible on printed messages, but requires different DSL expressions when obtaining field values.

Smart navigation resolves these problems by assuming reasonable defaults when repetitions or component operators are omitted:

  • If a repetition operator () is omitted, the first repetition of a group, segment or field is assumed
assert message.PATIENT_RESULT(0).PATIENT == message.PATIENT_RESULT.PATIENT // group
assert group.NK1(0)[5](0)[1].value == group.NK1[5](0)[1].value             // segment
assert group.NK1(0)[5](0)[1].value == group.NK1[5][1].value                // field
  • If a component is omitted, the first component or subcomponent of a composite is assumed
assert group.NK1(0)[5](0)[1].value == group.NK1[5].value
assert group.NK1(0)[2][1][1].value == message.NK1[2].value
  • Consequently, Smart Navigation also works with HL7 Null values:
assertEquals ''  , pid[11][1][1].value           // 'full' expression
assertEquals '""', pid[11][1][1].originalValue
assertTrue pid[11][1][1].isNullValue()
assertEquals ''  , pid[11].value                 // 'smart' expression
assertEquals '""', pid[11].originalValue
assertTrue pid[11].isNullValue()

Using smart navigation, the navigation expressions are usually shorter and less error-prone. Furthermore, in many cases the same expressions can be used for different HL7 versions that define new structures in a backward-compatible way.

Access target objects

Objects of the HAPI DSL layer internally reference objects defined in the HAPI ca.uhn.hl7v2.model package. These can be accessed via the target property.

ca.uhn.hl7v2.model.Segment hapiSegment = nk1.target

However, this is usually not needed because any property access or method call not applicable to HAPI DSL model objects is forwarded to target objects.

int cardinality1 = message.NK1(0).target.getMaxCardinality(3)
int cardinality2 = message.NK1(0).getMaxCardinality(3)         // equivalent
String segmentName1 = message.NK1(0).target.name
String segmentName2 = message.NK1(0).name                      // equivalent

Typically you need to reference the target object e.g. when passing control to code that is unaware of the HL7 DSL or explicitly requires HAPI classes.

Group and Segment emptyness (as of IPF 2.1)

Emptyness for segments and groups is defined as follows:

  • a segment is empty if all fields are empty
  • a group is empty if all contained groups and segments are empty

For brevity, the GroupAdapter and SegmentAdapter classes both implement an isEmpty() method.

assert msg1.PATIENT_RESULT.PATIENT.PV1.isEmpty() == false // not empty because some fields are filled
assert msg1.PATIENT_RESULT.PATIENT.PV2.isEmpty() == true  // empty because all fields are empty
assert msg1.PATIENT_RESULT.isEmpty() == false             // non empty because it contains a non-empty group

Iterative functions (as of IPF 2.1)

As HL7 messages are compound structures, you can imagine to iterate over them. Thus, the HL7 DSL implements iterators for HL7 messages and groups. Due to their nested structures, iteration is implemented as a depth first traversal over all non-empty substructures, i.e. non-empty groups and segments (see previous section).

An iterator() function is defined for the GroupAdapter and MessageAdapter classes. You seldomly will use iterator() directly, however, a lot of Groovy's iterative functions only rely on the existence of an iterator function. As a consequence, you can e.g. use the following Groovy functions on HL7 messages and groups:

  • each
  • eachWithIndex
  • every
  • any
  • collect
  • find
  • findAll
  • split
  • for statement
  • the spread operator

Some examples:

// Count the number of substructures
int numberOfStructures = 0
msg1.each { numberOfStructures++ }
println "The message has $numberOfStructures substructures"

// Check if there are any groups
boolean hasGroups = msg1.any { it instanceof GroupAdapter }

// A list of the names of all substructures
def names = msg1*.name

// For loop
for (def structure in msg1) {
  // do something with structure
}

// Find the first nested OBX segment
def obx = msg1.find { it.name == 'OBX' }
obx = msg1.findOBX() // shortcut notation

// Find all nested OBX segments
def obxList = msg1.findAll { it.name == 'OBX' }
obxList = msg1.findAllOBX() // shortcut notation

The find/findAll methods are handy in the following use cases:

  • accessing data in a deeply nested message structure that is not visible in the pipe-encoded representation.
  • uniformly accessing corresponding fields in messages with different structure
  • messages that have a group structure in a newer HL7 version while having a flat structure in previous versions.
def patientName = msg1.PATIENT_RESULT(0).PATIENT.PID[5][1].value
patientName = msg1.findPID()[5][1].value  // equivalent, shorter, and group-structure-agnostic

Manipulation

Message manipulation is as straightforward as navigation. You navigate to a segment or field and assign it a new object.

Manipulating segments

Currently you can change segments only, assignment to groups isn't supported yet.

msg1.EVN = msg2.EVN     // copy over EVN segment from msg2 to msg1
msg1.EVN.from(msg2.EVN) // equivalent

There's a dedicated method nrp(index) available for adding a repetitions to a repeating field

def newField = message.PATIENT_RESULT(0).PATIENT.NK1(0)[5].nrp(5) // Adds a repetition to NK1[5]

There are two caveats:

First, segments are copied with the assignment (i.e. =) operator only if the assignment operator follows a property read-access operation (via .property or ['property']). If you make an assignment directly to a segment variable, you assign object references.

def mySegment = ...
message.EVN = mySegment          // mySegment copied into message.EVN

def targetSegment = message.EVN
targetSegment = mySegment        // message.EVN remains unchanged, 
                                 // targetSegment and mySegment reference the
                                 // same object

Second, when you obtain a segment from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.

def mySegment = ...              // assignment to another NK1 segment instance
def group = message.PATIENT_RESULT(0).PATIENT
group.NK1(0) = 'abc'             // syntax error!
msg1.NK1(0) = mySegment          // syntax error!
msg1.NK1(0).from(mySegment)      // works!

Manipulating fields

To change a field value, navigate to the field (either by name or index, as shown above) and either assign it a string value or another field. Fields may also be changed by using the from() method.

def nk1      = message.PATIENT_RESULT(0).PATIENT.NK1(0)
def otherNk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0)
nk1[4]       = otherNk1[4]       // copy address
nk1[4][4]    = otherNk1[4][4]    // copy state or province only
nk1[4][4].from(otherNk1[4][4])   // equivalent

nk1[4][4]    = 'NY'              // set state or province directly

There are the same caveats with manipulating fields as with manipulating segments:
First, Composites are copied with the assignment (i.e. =) operator only if the assignment operator follows a subscript (i.e. []) operation. If you make an assignment to a composite variable directly you assign the respective object references

def otherXad = ...      // a contact address = HL7 composite type XAD
def nk1 = group.NK1(0)
nk1[4] = otherXad       // otherXad copied into nk1[4]

def xad = nk1[4]
xad = otherXad          // nk1[4] remains unchanged, xad and otherXad reference 
                        // the same object

Second, when you obtain a field from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.

def field = ...                // a primitive or composite field
def other = ...                // a primitive or composite field to be assigned
field(0) = 'abc'               // syntax error!
field(0) = other               // syntax error!
field(0).from(other)           // works for primitives and composites
field(0).value = 'abc'         // works for primitives only

Adding repetitions

Repetitions occur in HL7 groups, segments and fields. When creating a new message or manipulating an parsed message, it may become necessary to add a repeating element. A good example is the ORU_R01 message in HL7 v2.5, which includes nested repeatable groups, which in return contain repeatable segments that have repeatable fields.

There are two ways to add a repeating element: explicitly and implicitly.
Explicitly calling nrp() (for "new repetition") adds an element and returns it to the caller. The argument is of type String for repeating structures or int for repeating fields:

def message       = new MessageAdapter(new ORU_R01())
def patientResult = message.nrp('PATIENT_RESULT')          // add a PATIENT_RESULT group
def order         = patientResult.nrp('ORDER_OBSERVATION') // add a ORDER_OBSERVATION group
def observation   = order.nrp('OBSERVATION')               // add a OBSERVATION group
def obx5          = observation.OBX.nrp(5)                 // add a OBX-5 field

For consistency with HAPI, an element is also added if you access a repetition that does not exist yet.

def message       = new MessageAdapter(new ORU_R01())
def patientResult = message.PATIENT_RESULT(0)          // add a PATIENT_RESULT group
def order         = patientResult.ORDER_OBSERVATION(0) // add a ORDER_OBSERVATION group
def observation   = order.OBSERVATION(0)               // add a OBSERVATION group
def obx5          = observation.OBX[5](0)              // add a OBX-5 field
Index out of bounds!

The DSL does not distinguish whether the new repetition would be the next one to be created or not. If there's no PATIENT_RESULT group in the message, then msg.PATIENT_RESULT(8) does not silently add seven empty groups and returns the eighth! Instead only one group is added and returned, i.e. you actually obtain msg.PATIENT_RESULT(0).

Together with the Smart Navigation feature, it is particularly convenient that accessing a repeated element without index does a default to its first repetition. Hence, the code above can be condensed to:

def message = new MessageAdapter(new ORU_R01())
def obx5    = message.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION.OBX[5]

Rendering

Rendering writes the internal representation of a HL7 v2 message to its external representation, which is usually the ER7-encoded form with pipe field seperators.

To write a message to stdout, messages can be written to stream using the left-shift (<<) operator.

System.out << message

Otherwise, using the message variable in a string context or explicitly calling toString() does the same job:

assert message.toString() == "${message}"

Functional Extensions to HAPI

While the HL7 v2 DSL has its focus on providing a domain-specific syntax to navigate in HL7 messages and changing fields within messages, the functional extensions retrofit a couple of convenient functions on top of HAPI. By means of Groovy metaprogramming, however, it looks like these extensions are part of the HAPI API, i.e. you can call the methods on both the raw HAPI objects and the wrapper objects invisibly added by the HL7 v2 DSL.

It's important to note that the HL7 v2 DSL and the functional extensions do not depend on each other - you can employ any one or both feature sets as you like.

HL7 PipeParser and custom ModelClassFactory

In order to instantiate concrete implementations of Message, Group, Segment etc, the HAPI Parsers use a ModelClassFactory member object that looks up classes for these model components. The default implementation provides access to model components as specified in the HL7 specs.
In real world HL7 projects you frequently need to deal with non-standard HL7 "dialects" which are not covered by the specification and causes the parser to fail or generate "generic" model classes when used out-of-the-box. Although it's possible to implement a custom ModelClassFactory, there remains a lack of flexibility, e.g. it's not possible to use two distinct sets of "dialects" within one Java process. The HAPI extension library offers a solution for this limitation.

CustomModelClassFactory

The factory implementation org.openehealth.ipf.modules.hl7.parser.CustomModelClassFactory can be configured to map a HL7 version to a list of package names in which the HAPI model classes are looked up. If it fails to find the requested class, the call is delegated to HAPI's default implementation. Example:

def customModelClasses = ['2.5' : 
                            ['com.mycompany.profile1.hl7def.v25',
                             'com.mycompany.profile2.hl7def.v25']]
def customFactory = new CustomModelClassFactory(customModelClasses)

The following subpackages are looked up for the respective model classes:

model interface package
Message X.message
Group X.group
Segment X.segment
Type X.datatype

Note that the value side of the map is always a List. In the example above, the Message classes for version 2.5 are looked up in the following order:

  1. com.mycompany.profile1.hl7def.v25.message
  2. com.mycompany.profile2.hl7def.v25.message
  3. ca.uhn.hl7v2.model.v25.message (the default)

If you use a custom model class factory, it's strongly recommended to provide the factory instance to the HapiModelExtension extension class. This ensures that the various extensions that create HL7 message or structures (see below for details) use this factory to create the HL7 objects.

context.xml
...

    <!-- A custom model for HL7 v2.5 message -->
    <bean id="myModelClassFactory" class="org.openehealth.ipf.modules.hl7.parser.CustomModelClassFactory">
        <property name="customModelClasses">
            <map>
                <entry key="2.5">
                    <list>
                        <value>com.mycompany.profile1.hl7def.v25</value>
                        <value>com.mycompany.profile2.hl7def.v25</value>
                    </list>
                </entry>
                ....
            </map>
        </property>
    </bean>

...

    <!-- HAPI extensions -->
    <bean id="hapiModelExtension" 
        class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
        <property name="mappingService" ref="..." />
        <property name="factory" ref="myModelClassFactory"/>
    </bean>

...

Custom PipeParser

The PipeParser implementation provided by this module (org.openehealth.ipf.modules.hl7.parser.PipeParser) by default uses the CustomModelClassFactory and a default set of Primitive validation rules (see HL7 v2 Message Validation chapter for details). Apart from that it does not add any features to the HAPI PipeParser class.

import org.openehealth.ipf.modules.hl7.parser.PipeParser;

...
def customParser = new PipeParser(customFactory)

Methods added to the HAPI Message interface

New Messages

You can create a new message from scratch by specifying event type, trigger event and version. Its message header fields are populated with the event type, trigger event, version, the current time as message date, and the common separators.

import ca.uhn.hl7v2.model.Message

// Static method extension to the HAPI Message class
def msg = Message.ADT_A01('2.5') // creates a ca.uhn.hl7v2.model.v25.message.ADT_A01 object
HL7 Message Structures

The message structure is a data structure that expresses an association of a message type with an event for a class of HL7 messages. Each message structure also contains a unique ID, e.g. ADT_A01.
Beginning with HL7 v2.4, the combination of event type and trigger event is NOT necessarily the message structure ID, e.g. a ADT^A04 message in version 2.5 has the message structure ADT_A01.
Note that HAPI message classes are in fact message structure classes, i.e. to correctly create a ADT^A04 v2.5 message, you need to instantiate an object of class ca.uhn.hl7v2.model.v25.message.ADT_A01 and set the MSH-9 field to ADT^A04^ADT_A01.

When you create new messages using IPF's HAPI extensions, the message structure (and therefore the HAPI class to be used) is automatically derived from event type, trigger event and message version, and MSH-9 is populated accordingly. Example:

import ca.uhn.hl7v2.model.Message

def msg = Message.ADT_A04('2.5')
assert msg instanceof ca.uhn.hl7v2.model.v25.message.ADT_A01

Acknowledgements and Responses

You can create positive or negative acknowledgments to HL7 messages with a single method call. The acknowledgment message

  • is in the same HL7 version as the original message
  • refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
  • contains the current timestamp as message date
  • is populated with MSA and/or ERR segments as specified in the parameters.
// Positive Acknowledgement
def ack  = msg.ack()
// Negative Acknowledgements
def nak1 = msg.nak('Reason for failure', AckTypeCode.AE)
def nak2 = msg.nak(new HL7Exception('reason for failure', 204), AckTypeCode.AE)

In case of parsing errors there's no message object available to derive the negative acknowledgment from. In this case you can reuse the Exception thrown by the parser to create a generic negative acknowledgement of a specific version.

def nak = ca.uhn.hl7v2.model.Message.defaultNak(e, AckTypeCode.AE, '2.5') // NAK of version 2.5

Generating acknowledgments is only a special case of generating a response to an original message. If a response is defined as dedicated HL7 message as with responses to Query messages, you have to use the respond(eventType, triggerEvent) extension method. The response message

  • is in the same HL7 version as the original message
  • refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
  • contains the current timestamp as message date
  • has a populated MSA segment
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message

Message checks

Use the matches extension method to check for specific message types

if (msg.matches('ADT','A01','2.5')) {
   // true if msg is ADT_A01 version 2.5
} else if (msg.matches('ADT','*','*')) {
   // true if msg from ADT domain of any version
}

You can check the three parameters of matches individually, too:

def version = msg.version           // 2.5
def eventType = msg.eventType       // ADT
def triggerEvent = msg.triggerEvent // A01

Message dump

For debugging purposes, it's often useful to know the internal (hierarchical) data structure of a HAPI Message. For complex messages, the returned structure can be pretty extensive, so you should avoid using this in production environments:

println msg.dump()

Methods added to the HAPI Structure interface

New segments

Just as creating a message, you can also create a segment by calling its respective name as static method on the ca.uhn.hl7v2.model.Structure interface. You need to pass the enclosing Message object as argument, which determines the HL7 version to be used.

import ca.uhn.hl7v2.model.Segment
...
// Static method extension to the HAPI Message class
def obx = Segment.OBX(msg) // creates a ca.uhn.hl7v2.mode.v25.segment.OBX object
//  obx = Segment.OBX(msg.target) if msg is a MessageAdapter

Printing structures

All HAPI Structures (i.e. not only Messages, but also arbitrary Groups and Segments) can be converted into their pipe-encoded representation by calling the encode() extension method. Note that a Message is a subclass of Group.

assert message.MSH.encode() == 'MSH|^~\\&|SAP-ISH|HZL|||20040805152637||ADT^A01|123456|T|2.2|||ER'
println message.encode() // prints the complete message

Methods added to the HAPI Type interface

New fields

Just as creating a message or segment, you can also create a field by calling its respective name as static method on the ca.uhn.hl7v2.model.Composite or ca.uhn.hl7v2.model.Primitive}}interface. You need to pass the enclosing {{Message object as argument, which determines the HL7 version to be used.
Composites may be initialized with a map containing the component values. Primitives may be initialized with a literal string value.

import ca.uhn.hl7v2.model.Composite
import ca.uhn.hl7v2.model.Primitive
...
// Static method extension to the HAPI Composite class
def ce = Composite.CE(msg, [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM'])
//  ce = Composite.CE(msg.target, ...) if msg is a MessageAdapter
// Static method extension to the HAPI Primitive class
def st = Primitive.ST(msg, 'value')
//  st = Primitive.ST(msg.target, 'value') if msg is a MessageAdapter

Printing types

All HAPI Types (i.e. Primitives, Composites, and Varies) can be converted into their pipe-encoded representation by calling the encode() extension.

assert message.MSH.messageType.encode() == 'ADT^A01'

// Together with the HL7 DSL, you can also write
assert message.MSH[9].encode() == 'ADT^A01'

Mapping Service

The Mapping Service has been moved to the IPF Core features. After all, although often used in HL7 processing, code system mapping is not a feature that is inherently exclusive for HL7. Please refer to the Mapping Service chapter.
What remains specific to IPF's HL7 v2 support, however, is that the mapping extensions can be applied directly on all HAPI types. The encode() extension is called before the mapping is executed.

Given the following mapping example:

example.map
mappings = {
		 encounterType(['2.16.840.1.113883.12.4','2.16.840.1.113883.5.4'],
			 	E : 'EMER',
			  	I : 'IMP',
			  	O : 'AMB'
		 )

		 vip(['2.16.840.1.113883.12.99','2.16.840.1.113883.5.1075'],
				Y      : 'VIP',
				(ELSE) : { it }
		 )

              	 messageType(
                                'ADT^A01' : 'PRPA_IN402001'
                                (ELSE) : { throw new HL7Exception("Invalid message type", 207) }
                 )
}

You can use the mapping functions directly on composite or primitive field objects:

// Mapping primitives
assert msg.PV1.patientClass.value == 'I'
assert msg.PV1.patientClass.map('encounterType') == 'IMP'
assert msg.PV1.patientClass.mapEncounterType() == 'IMP'

// Together with the HL7 v2 DSL, you can also write
assert msg.PV1[2].mapEncounterType() == 'IMP'

// To map a Composite field, you can write
assert msg.MSH.messageType.mapMessageType() == 'PRPA_IN402001'
assert msg.MSH[9].mapMessageType() == 'PRPA_IN402001'

Examples for HL7 Messaging

This section shows the HL7 v2 DSL and Functional Extensions to HAPI in action. As the primary purpose of DSL and extensions is to augment the original HAPI library, it seems appropriate to take HAPI examples code and reimplement them.

Create a message from scratch

The first example is to create a simple message from scratch, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/CreateAMessage.html.

The example shows

  • how to use Groovy properties instead of getter methods for named access to fields
  • how to use IPF's functional extension to create messages with a prefilled MSH segment
  • how to employ Groovy's with clause.
CreateAMessage.groovy

import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension
import org.openehealth.ipf.modules.hl7.parser.PipeParser
import ca.uhn.hl7v2.model.*

public class CreateAMessage{
        
    static def makeMessage() {
        def msg = Message.ADT_A01('2.4')
        // 'with' is a Groovy feature that delegate all unknown method properties
        // in this case to the msg.MSH object.
        msg.MSH.with {
            sendingApplication.namespaceID.value = 'TestSendingSystem' 
            sequenceNumber.value = '123'
        }
        msg.PID.with {
            getPatientName(0).familyName.surname.value = 'Doe'
            getPatientName(0).givenName.value = 'John'
            getPatientIdentifierList(0).ID.value = '123456'
        }
        println "Printing ER7 Encoded Message:"
        println new PipeParser().encode(msg)
    }
    
    /**
     * @param args
     */
    public static void main(def args) {
        // Initialize the MetaClass extension
        ExpandoMetaClass.enableGlobally()
        new HapiModelExtension().extensions.call()
        // 
        makeMessage()
    }
}

The console output is:

Printing ER7 Encoded Message:
MSH|^~\&|TestSendingSystem||||20090923152839||ADT^A01^ADT_A01|1956|P^T|2.4|123
PID|||123456||Doe^John

The message timestamp will vary as it reflects the point of time when the message is being created.

Create a ORU_R01 v2.5 message

The second example is to create a not-so-simple message, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/PopulateOBXSegment.html. The problem about this message is its nested structure of repeatable elements.
This time, the implementation also takes advantage of the HL7 v2 DSL, replacing all named field accessors by their position-based syntax.

In addition to the first example, this example shows

  • how to wrap a HAPI message into a HL7 v2 DSL MessageAdapter object
  • how to apply the HL7 v2 DSL
  • how to transparently add repetition to repeatable structures and fields
  • how to omit default indices due to the DSL's Smart Navigation feature
  • how to create composite and primitive fields from scratch
  • how to work with "Varies" types like in OBX-5
PopulateOBXSegment.groovy

import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension
import org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension
import org.openehealth.ipf.modules.hl7dsl.MessageAdapter

import ca.uhn.hl7v2.model.*

public class PopulateOBXSegment{
    
    static def makeOBX() {
        // Create message and wrap
        def msg = new MessageAdapter(Message.ORU_R01('2.5'))
        
        // Populate OBR. Group repetitions are created while navigating
        def obr = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBR
        obr[1] = '1'
        obr[3][1] = '1234'
        obr[3][2] = 'LAB'
        obr[4] = '88304' // Smart Navigation expands this to obr[4][1]

        // Populate the first OBX
        // Note that we don't specify the repetition with PATIENT_RESULT and
        // ORDER_OBSERVATION because Smart Navigation
        def obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(0).OBX
        obx[1] = '1'
        obx[2] = 'CE' // the type of OBX-5
        obx[3] = '88304'
        obx[4] = '1'
        def ce = Composite.CE(msg.target, 
           [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM'])
        // OBX-5 is a repeatable Varies field. 
        // Don't care about it, the DSL gets it right for you.
        obx[5] = ce  // equivalent with obx[5](0).data = ce
        
        // Populate the second OBX
        obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(1).OBX
        obx[1] = '2'
        obx[2] = 'TX' // the type of OBX-5
        obx[3] = '88304'
        // The second OBX in the sample message has an extra subcomponent at
        // OBX-3-1. This component is actually an ST, but the HL7 specification allows
        // extra subcomponents to be tacked on to the end of a component. This is
        // uncommon, but HAPI nontheless allows it.
        obx[3][1].extraComponents[0].data = Primitive.ST(msg.target, 'MDT')
        obx[4] = '2'
        def tx = Primitive.TX(msg.target, 
           'MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE')
        obx[5] = tx
        
        // Return the message
        msg
    }
    
    public static void main(def args){
        // This example requires both extension packages
        ExpandoMetaClass.enableGlobally()
        new HapiModelExtension().extensions.call()
        new Hl7ModelExtension().extensions.call()
        println makeOBX()
    }
}

The console output is:

MSH|^~\&|||||20090923151109||ORU^R01^ORU_R01|98|P^T|2.5
OBR|1||1234^LAB|88304
OBX|1|CE|88304|1|T57000^GALLBLADDER^SNM
OBX|2|TX|88304&MDT|2|MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE

Conclusion

The example showed that with IPF's HL7 support you can work with HL7 messages without ever seeing and touching much of the programming interface of the underlying HAPI library. The code is readable for HL7 v2 domain experts without much knowledge in programming.
This impressively underlines the purpose of domain specific languages to narrow the gap between domain experts and software development experts, here applied to the domain of HL7 v2 messaging.

Language reference

Message elements

Element Implementation
message org.openehealth.ipf.modules.hl7dsl.MessageAdapter
group org.openehealth.ipf.modules.hl7dsl.GroupAdapter
segment org.openehealth.ipf.modules.hl7dsl.SegmentAdapter
composite field org.openehealth.ipf.modules.hl7dsl.CompositeAdapter
primitive field org.openehealth.ipf.modules.hl7dsl.PrimitiveAdapter
undefined field org.openehealth.ipf.modules.hl7dsl.VariesAdapter
repeating group groovy.lang.Closure containing groups and/or messages
repeating segment groovy.lang.Closure containing segments
repeating field groovy.lang.Closure containing composites and or primitives

Also refer to understanding repetitions for an introduction to repeating groups, segments and fields.

Read access operations

Read access operations on non-repeating message elements

The following table specifies the effect of operators for read-access operations on non-repeating message elements.

Type . operator (dot) [] operator (subscript)
message or group Access to group or segment by name:
message.<groupName>   // contained group with name <groupName>
message.<segmentName> // contained segment with name <segmentName>
Access to group or segment by name:
message['<groupName>']   // contained group with name <groupName>
message['<segmentName>'] // contained segment with name <segmentName>
segment Access to field by symbolic field name:
segment.<symbolicFieldName> // field of segment with name <symbolicFieldName>
Example:
message.MSH.sendingApplication // 3rd field of MSH segment
Access to field by symbolic field name or index:
segment['<symabolicFieldName>'] // field of segment with name <symbolicFieldName>
segment[i]                      // i-th field of segment (i=1..n)
Example:
message.MSH['sendingApplication'] // 3rd field of MSH segment
message.MSH[3]                    // 3rd field of MSH segment
composite field N/A Access to component
composite[i] // i-th component of composite (i=1..n)
Example:
message.NK1(0)[4][4] // 4-th component of composite field message.NK1(0)[4]
primitive field Access to primitive field's string value
field.value          // the field's string value ('""' removed)
field.originalValue  // the field's original value
N/A

Read access operations on repeating message elements

The following table specifies the effect of the () operator for read-access operations on repeating message elements.

Type () (closure call)
repeating group Access to group repetitions and its members
groups() // list of groups in repetition
groups(i) // i-th group in repetition (i=0..n)
Example:
message.PATIENT_RESULT() // all groups of the repeating PATIENT_RESULT group
message.PATIENT_RESULT(0) // first group of the repeating PATIENT_RESULT group
repeating segment Access to segment repetitions and its members
segments() // list of segments in repetition
segments(i) // i-th segment in repetition (i=0..n)
Example:
message.NK1() // all segments of the repeating NK1 segment
message.NK1(0) // first segment of the repeating NK1 segment
repeating field Access to field repetitions and its members
fields() // list of fields in repetition
fields(i) // i-th field in repetition (i=0..n)
segment.count(j) // returns the number of repetitions of the j-th field
segment.nrp(j) // adds a repetition to the j-th field, returning the new object
Example:
message.NK1(0)[5]() // all fields of the repeating message.NK1(0)[5] field
message.NK1(0)[5](1) // second field of the repeating message.NK1(0)[5] field

Write access operations

The following table specifies the effect of operators for write-access operations on non-repeating message elements.

Type .\ operator (dot) []\ operator (subscript)
message or group Copy group or segment by name:
message.<groupName> = group // where group.name == groupName
message.<segmentName> = segment // where segment.name == segmentName
Copy group or segment by name:
message['<groupName>'] = group // where group.name == groupName
message['<segmentName>'] = segment // where segment.name == segmentName
segment Set primitive field by symbolic field name:
segment.<symbolicFieldName> = value // value is a string

Copy composite or primitive by symbolic field name:

segment.<symbolicFieldName> = composite
segment.<symbolicFieldName> = primitive

Example:

messsage1.MSH.sendingApplication = 'XYZ'
Set primitive by symbolic field name or index:
 segment['<symbolicFieldName>'] = value // value is a string
 segment[i] = value                     // value is a string
Copy composite or primitive by symbolic field name or index:
 segment['<symbolicFieldName>'] = composite
 segment['<symbolicFieldName>'] = primitive
 segment[i] = composite
 segment[i] = primitive
Example:
message1.MSH['sendingApplication'] = 'XYZ'
message1.MSH[3] = 'XYZ'
message1.EVN[7] = message2.EVN[7]
composite field N/A Set primitive by index:
composite[i] = value // value is a string
Copy component or primitive by index:
 composite[i] = component // non-primitive
 composite[i] = primitive
Example:
message1.NK1(0)[4][4] = 'abc'
message1.NK1(0)[4][4] = message2.NK1(0)[4][4]
primitive field Set primitive's value
field.value = value // value is a string
N/A

Write access operations on repeating message elements

Direct write access to repeating elements is not possible. Write access to message elements obtained via the () operator must be done via the from() method. If the obtained element is a primitive field then you may also use the .value property to assign strings.

Write access operations on message elements defined as separate variable

When using the = operator for write access on message elements defined as separate variable then the usual Java/Groovy semantics apply. In this case the variable will just reference another object. Only if the = operator follows a [] (subscript) or . (property access) operation the assignment operator will cause a copy operation.

Method and property dispatch

  • Any property access not processed by an adapter is dispatched to the target object.
  • Any method call not processed by an adapter is dispatched to the target object.
  • Returned objects from the target are adapted if a corresponding adapter exists.

Access to special objects

  • The adapted target objects can be obtained from any adapter via adapter.target.
  • The corresponding message adapter to a structure or field adapter can be obtained via adapter.message
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.