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.
- 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> ...
- 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> ...
- 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:
- com.mycompany.profile1.hl7def.v25.message
- com.mycompany.profile2.hl7def.v25.message
- 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.
...
<!-- 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. 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:
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.
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
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> 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) 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) 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) 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) 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 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 segment['<symbolicFieldName>'] = composite segment['<symbolicFieldName>'] = primitive segment[i] = composite segment[i] = primitive 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 composite[i] = component // non-primitive composite[i] = primitive 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