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
<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.
<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:
<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> 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