HAPI DSL

HAPI DSL

HAPI DSL (modules-hl7dsl component) is a library that implements a domain-specific language (DSL) for manipulating HL7 version 2 messages. The DSL is based on Groovy and internally makes use of the HAPI library. Using modules-hl7dsl, HL7 message processing in IPF applications becomes almost trivial.

Camel integration

Refer to the DSL extensions section of the HL7 processing page for an example how to use the HAPI DSL within Camel routes. The modules-hl7dsl library may also be used standalone i.e. independent of Apache Camel and other IPF components in Groovy applications.

Getting started

This section explains how to get started with the modules-hl7dsl library. It explains what to include in Maven 2 project descriptors (pom.xml files) and presents some usage examples. For further examples you may also want to look at the modules-hl7dsl unit tests. A more formal description of the HAPI DSL is given in the language reference section.

Maven setup

For setting up Maven follow the instructions on the IPF development page. If you want to use the the HAPI DSL standalone in your Groovy projects then you only need to include

pom.xml for standalone use
<dependency>
    <groupId>org.openehealth.ipf.modules</groupId>
    <artifactId>modules-hl7dsl</artifactId>
    <version>${ipf-version}</version>
</dependency>

in your pom.xml file. For using the HAPI DSL inside Camel routes you additionally need to include the following dependency.

pom.xml for use with Camel
<dependency>
    <groupId>org.openehealth.ipf.platform-camel</groupId>
    <artifactId>platform-camel-hl7</artifactId>
    <version>${ipf-version}</version>
</dependency>

where ${ipf-version} must be replaced with the IPF version you want to use.

As of IPF 1.7-m3, the underlying HAPI library has been updated. The HL7 version-dependent classes have to be included separately:

pom.xml for using HL7 version 2.5 and 2.5.1
<dependency>
    <groupId>ca.uhn.hapi</groupId>
    <artifactId>hapi-structures-v25</artifactId>
    <version>0.6</version>
</dependency>
<dependency>
    <groupId>ca.uhn.hapi</groupId>
    <artifactId>hapi-structures-v251</artifactId>
    <version>0.6</version>
</dependency>

Despite the extra configuration, it reduces the overall size of your project as the other version's HL7 classes are not included any more.

Message construction

What follows are some examples how to use the modules-hl7dsl component. First we create a message from an HL7 file on the classpath.

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

def message = load('msg-02.hl7')

Alternatively, the adaptor 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 also easily create a message as a copy of an existing message:

def messageCopy = message.copy()

Navigate to structures

In the following example we obtain the PID segment contained in the PATIENT group which is again contained in the first element of the repeatable PATIENT_RESULT group. In the HL7 world it is common to use parentheses i.e. () to refer to an elements in a repetition. The same notation is supported in the HAPI DSL.

def segment = message.PATIENT_RESULT(0).PATIENT.PID

Navigate to fields

Obtaining fields is similar to obtaining structures except that fields are often referred to by number instead of by name. To obtain the MSH-3 field from a message we can write:

def composite = message.MSH[3]

This field is a composite (by specification) and we want to obtain a primitive component e.g. the second one:

def primitive = message.MSH[3][2]

or, equivalently,

def primitive = message.MSH.sendingApplication.universalIDType

As shown above, it's also possible to navigate by specifying the field names instead of the number. 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 we don't know the version of the HL7 message in advance, we 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

Fields may repeat. To obtain a certain element of a repeating field we use parentheses like with structures. In the next example we obtain the first element of the repeating NK1-5 field. Since NK1 is also a repeating segment we have to define which NK1 segment to use.

def field = message.NK1(0)[5](0)

To get a list of elements of a repeating field we omit the index.

def fieldList = message.NK1(0)[5]()

Getting field values

Use the value property to obtain the value of a primitive field, or simply call toString()

String primitiveValue = message.MSH[3][2].value
primitiveValue = message.MSH[3][2].toString()

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()

Smart navigation

Smart navigation is available starting from IPF 1.6

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.

  • Repetitions of groups or segments, e.g. associated parties:
def nextOfKin = message.NK1(0)
  • Repetitions of fields, e.g. phone number(s) of associated parties:
def phoneNumber = message.NK1(0)[5](0)[1]
  • "Hidden" composites, i.e. fields where most often only the first component is used, e.g. composite type FN used for the family name of associated parties:
def familyName = message.NK1(0)[2][1][1]

To make things worse, the internal structure changes between HL7 versions. In higher versions, e.g. primitive fields are 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 this problem by assuming reasonable defaults when repetitions or component operators are omitted:

  • If a repetition is omitted, the first repetition of a group, segment or field is assumed
assert message.NK1(0)[5](0)[1].value == message.NK1[5](0)[1].value
assert message.NK1(0)[5](0)[1].value == message.NK1[5][1].value

assert message.PATIENT_RESULT(0).PATIENT.PID[5][1] == message.PATIENT_RESULT.PATIENT.PID[5][1]
  • If a component is omitted, the first component or subcomponent of a composite is assumed
assert message.NK1(0)[5](0)[1].value == message.NK1[5].value
assert message.NK1(0)[5](1)[1].value == message.NK1[5](1).value
assert message.NK1(0)[2][1][1].value == message.NK1[2].value
  • The treatment of HL7 null values also apply to Smart Navigation Expressions:
assertEquals '', PID[11][1][1].value
assertEquals '""', PID[11][1][1].originalValue
assertEquals '', PID[11].value
assertEquals '""', PID[11].originalValue
assertTrue PID[11][1][1].isNullValue()
assertTrue PID[11].isNullValue()

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

Change structures

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

msg1.EVN = msg2.EVN

copies the EVN segment from msg2 over to msg1. We can also use the from() method.

msg1.EVN.from(msg2.EVN)

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 the usual Java/Groovy semantics apply.

def mySegment = ...
msg1.EVN = mySegment // mySegment copied onto msg1.EVN

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

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. Lets say msg1.NK1 refers to a repeating segment:

def mySegment = ...
msg1.NK1(0) = 'abc' // syntax error
msg1.NK1(0) = mySegment // syntax error
msg1.NK1(0).from(mySegment) // works

Change fields

To change a field value we navigate to that field (either by name or index, as shown above) and either assign it a string or another field.

def msh = message.MSH
def nk1 = message.NK1(0)
msh[5] = nk1[4][4]
msh[5] = 'abc'

Composite fields may also be changed by assigning other composite fields or using the from() method.

def msg1 = load('msg-01.hl7')
def msg2 = load('msg-02.hl7')
// alternative 1 using assignment
msg1.NK1(0)[4] = msg2.NK1(0)[4]
// alternative 2 using from()
msg1.NK1(0)[4].from(msg2.NK1(0)[4])

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 the usual Java/Groovy semantics apply.

def myComposite = ...
def mySegment = msg1.NK1(0)
mySegment[4] = myComposite // myComposite copied onto mySegment[4]

def targetComposite = mySegment[4]
targetComposite = myComposite // mySegment[4] remains unchanged, targetComposite and myComposite reference the same object

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. Lets say msg1.XYZ[1] refers to a repeating field:

def primitive = msg1.MSH[5]
msg1.XYZ[1](0) = 'abc' // syntax error
msg1.XYZ[1](0) = primitive // syntax error
msg1.XYZ[1](0).value = 'abc' // works for primitives
msg1.XYZ[1](0).from(primitive) // works for primitives and composites

The same is true for adding repetitions to a field using the nrp() function.

def field = msg1.XYZ.nrp(1) // adds a repetition to a repeatable field
msg1.XYZ.nrp(1) = 'abc'  // Syntax error
msg1.XYZ.nrp(1).value = 'abc' // works for primitives
msg1.XYZ.nrp(1).from(primitive) // works for primitives and composites

Access target objects

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

ca.uhn.hl7v2.model.Segment hapiSegment = message.NK1(0).target

However, this is usually not needed because any property access or method call not applicable to HAPI DSL model objects is dispatched to target objects. For example, to invoke the getMaxCardinality(int) method on a target segment, just write

int cardinality = message.NK1(0).getMaxCardinality(3)

This is equivalent to

int cardinality = message.NK1(0).target.getMaxCardinality(3)

The same is true for properties on the target object.

String segmentName = message.NK1(0).name

is equivalent to

String segmentName = message.NK1(0).target.name

Render messages

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

System.out << message

Understanding repetitions

Any access to a repeating group, segment or field returns a groovy.lang.Closure object that maintains a list of repeating elements.

def repeatingGroup = message.PATIENT_RESULT // closure representing a repeating group

Using parentheses i.e. () calls the closure. Using a positive integer i as argument returns the i-th member of the repetition.

def group = repeatingGroup(0) // first element of the repetition (a group)

Using no argument returns all elements of the repetition.

def groups = repeatingGroup() // all elements of the repetition (a group list)

Repetitions of fields can be counted and appended to.

def count = segment.count(i) // returns the number of repetitions of the i-th field of the segment
def field = segment.nrp(i) // adds a repetition to the i-th field, returning the new and empty element

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.