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 (with auto-completion support in Eclipse)
- 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.
To use the DSL, first a org.openehealth.ipf.modules.hl7dsl.MessageAdapter object must be constructed. That MessageAdapter object is a wrapper (adapter) of a HAPI message object and provides the DSL methods for navigation, manipulation and rendering. It is also valid to call the methods of the HAPI object on the adapter object.
Once constructed, the MessageAdapter itself is responsible to return adapters (wrappers) for the HAPI types and structures being accessed, like org.openehealth.ipf.modules.hl7dsl.SegmentAdapter and org.openehealth.ipf.modules.hl7dsl.GroupAdapter, thus delegating the DSL features to adapters of the accessed structures.
The start point of the HL7 DSL is the MessageAdapter. Use load to construct a message adapter from an HL7 file on the classpath or from an InputStream.
Alternatively, a message adapter can be created from a string representation of a HL7 message directly:
You can easily create a message adapter as a copy of an existing message:
If you have a native HAPI message object, you can wrap it manually:
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.).
|Auto-completion support in Eclipse|
The DSL comes with auto-completion support for the Eclipse IDE. Using the auto-completion you do not need to have profound knowledge of the HL7 specifications when working with HL7 messages. To activate the auto-completion support, you must specify the wraped HAPI structure in the message adapter generics type definition (e.g. MessageAdapter<ORU_R01> message = ... ORU_R01 is the wrapped HAPI message). If you declare the message with def keyword, the auto-completion support will not be activated for that message.
Groups and Segments can be accessed by name like an object property.
Groovy doesn't require to specify the exact type of a variable, instead you can use the def keyword. For HL7 v2 processing, this might be a very convenient feature that saves you many explicit type checks and type casts. However, if you do not specify the types you will not be able to benefit the HL7 DSL auto-completion support in Eclipse and will loose type information that may help you later on refactoring.
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:
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 you don't know the version of the HL7 message in advance, better use the more concise index notation. Example:
Field variables render to their string encoding e.g. when printed, by implementing an appropriate toString() method. However, for literal comparison use the encode() method, and for variable assignment use the value property (currently not shown for auto-completion).
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
- encode() returns the double quotes
- isNullValue() returns true, if the original value of the field was "".
Therefore, if PID(0) (first Street or Mailing Address) was "", the following assertions are true:
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.
To get a list of elements of a repeating structure, simply omit the index so that it looks like a method call without parameters.
Furthermore, repetitions can be counted:
Currently there is no auto-completion support for the 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:
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
- If a component is omitted, the first component or subcomponent of a composite is assumed
- Consequently, Smart Navigation also works with HL7 Null values:
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.
getPath() is defined for all HL7 DSL Adapter classes shows you the current element you have navigated to, with the HL7 DSL (currently works only with the field-like access). For example, when you validate a message you may later use the path to indicate which part of the message are not valid.
isEmpty() 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.
As of IPF version 2.2.6, all HL7 DSL Adapter classes implement this method.
Objects of the HAPI DSL layer internally reference objects defined in the HAPI ca.uhn.hl7v2.model package. These can be accessed via the getTarget() method or the read-only target property.
However, explicite access to target is usually not needed because any property access or method call not applicable to HAPI DSL model objects are forwarded to their target objects.
On the message level, the read-only property hapiMessage can be used to access the HAPI Message object wrapped into the given MessageAdapter.
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:
- for statement
- the spread operator
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.
Message manipulation is as straightforward as navigation. You navigate to a segment or field and assign it a new object.
Currently you can change segments only, assignment to groups isn't supported yet.
There's a dedicated method nrp(index) available for adding a repetitions to a repeating field
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.
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.
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.
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
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.
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:
For consistency with HAPI, an element is also added if you access a repetition that does not exist yet.
|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:
Type of data contained in the fifth field of each OBX segment is variable and determined by the second field. In other words, when OBX-2 equals to "CE", OBX-5 contains repeatings of CE elements. To set data type in a newly created OBX segment or to change the type in an existing segment, the following approach can be used:
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.
Otherwise, using the message variable in a string context or explicitly calling toString() does the same job: