HL7 processing
Support for HL7 message processing in IPF is provided by several IPF components. This is summarized in the following table.
| Component | Description | Documentation |
|---|---|---|
| modules-hl7dsl | Implements a Groovy DSL on top of the HAPI library. | HAPI DSL |
| modules-hl7 | Provides functional extensions to the HAPI library. | HAPI extensions HL7 validation |
| platform-camel-hl7 | Provides HL7 extensions to the Camel DSL. | DSL extensions |
An overview of the components can be found on the IPF architecture page. For a detailed description follow the links in the Documentation column. The components modules-hl7 and modules-hl7dsl can be used independently of Apache Camel in Groovy projects. The platform-camel-hl7 component is Camel-specific and makes the HAPI DSL, HAPI extensions and HL7 validation available in IPF route definitions. The extensions provided by platform-camel-hl7 and modules-hl7 can be activated via the DSL extension mechanism (see next section).
| HL7 features of Camel and IPF Extensions provided by platform-camel-hl7 are complementary to features provided by the camel-hl7 component (available since Camel version 1.5). |
| HL7 tutorial The HL7 message processing tutorial guides you through a simple IPF HL7 application using some features described in this section. |
Configuration
Refer to the IPF Scripting Layer page if you need an introduction how to configure IPF applications that use the DSL extension mechanism. In this section you'll see how to activate the extensions provided by the components platform-camel-hl7 and modules-hl7. The extension classes of these components are listed in the following table.
| Component | Extension class |
|---|---|
| platform-component-camel | org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension |
| modules-hl7 | org.openehealth.ipf.modules.hl7.extend.HapiModelExtension |
Here's an example Spring application context XML file:
<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://activemq.apache.org/camel/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://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext" /> ... <bean id="routeBuilder" depends-on="routeModelExtender" class="..." /> <bean id="hapiModelExtension" class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension"> <property name="mappingService" ref="..." /> </bean> <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension"> </bean> <bean id="hl7ModelExtension" class="org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="hapiModelExtension" /> <ref bean="coreModelExtension" /> <ref bean="hl7ModelExtension" /> </list> </property> </bean> ... </beans>
In this example, we also use the extensions provided by the platform-camel-core component (coreModelExtension bean), otherwise, the following example wouldn't work. For details about the mappingService property of the hapiModelExtension bean refer to the mapping service section on the HAPI extensions page.
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
As of IPF 1.7.0, if you try access a repetition of a field, segment or group that does not exist yet, it is automatically added to the repeating element, i.e. nrp(index) is implicitly called. This also works in the context of Smart Navigation when you omit the default index (0) and there's no repetition yet.
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
HAPI Extensions
HAPI extensions is a module that adds methods to the original HAPI library in order to access and manipulate HL7 version 2 messages more conveniently. The library is based on Groovy. It can be used directly with HAPI or together with the HAPI DSL.
Integration with HAPI DSL
HAPI DSL adapters forward all unknown method calls to their underlying HAPI model classes, which are extended by this library. Therefore you can easily access these extensions within the HAPI DSL. The reason for splitting the two libraries is while HAPI DSL focuses on providing a 'fluent' HL7 language, the HAPI Extensions module provides access to operations specific to the HL7 domain, e.g. creating an acknowledgment to a given message. Note that the two libraries do not depend on each other and can therefore be used independently, although they complement each other perfectly.
Getting Started
Maven Setup
For setting up Maven follow the instructions on the IPF development page. If you want to use the the HAPI extensions standalone in your Groovy projects, then you only need to include
<dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-hl7</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. For instructions how to activate the HAPI extensions inside your IPF application refer to the configuration section on the HL7 processing page.
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.
HL7 Parser and 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, the HAPI parsers can not be configured to use it. 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)
Custom PipeParser
Use the PipeParser implementation provided by this module (org.openehealth.ipf.modules.hl7.parser.PipeParser), which can be configured with a custom model factory
def customParser = new PipeParser(customFactory)
In Camel integration scenarios, use the custom parser instance by adding a parameter. Also refer to the Camel HL7 extension reference
...
from(...)
.unmarshal()
.ghl7(customParser)
...
Mapping Service
The org.openehealth.ipf.modules.hl7.mapping.MappingService interface deals with the requirement that processing HL7 messages often involves mappings between code systems, i.e. from one set of codes into a corresponding set of codes. For example, HL7 version 2 to HL7 version 3 use different code systems for most coded values like message type, gender, clinical encounter type, marital status codes, address and telecommunication use codes, just to mention a few. MappingService implementations provide the mapping logic, which can be a simple java.util.Map, but can also be a facade for a remote terminology service.
The modules-hl7 component extends the java.lang.String and some of the HAPI classes to map between values. Please refer to the API Extensions section below for examples.
The HL7 library provides one MappingService implementation (BidiMappingService), which implements
- bidirectional mapping
- mapping of arbitrary objects
- definitions of mappings using external Groovy Scripts
To use BidiMappingService, you have to initialize it with the external Groovy resource, e.g. using Spring:
<!-- Groovy class that provides the operations on the mappings --> <bean id="hl7MappingService" class="org.openehealth.ipf.modules.hl7.mappings.BidiMappingService"> <property name="mappingScript" value="classpath:example.groovy"/> </bean>
The mapping example is displayed below:
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) }
)
}
This defines three mappings (encounterType, vip, and messageType), having an optional definition for OIDs for the key and value code systems. The encounterType mapping has three entries, while the vip and messageType mappings have only one.
The ELSE entry is called on MappingService.get() request with unknown keys. ELSE can be
- a Closure, which takes the key as parameter and is then executed
- any other Object o, which will return o.toString().
In the example above,
- for the vip mapping the key is returned, so that mappingService.get('vip', 'X') == 'X'
- for the messageType mapping, an HL7Exception is thrown.
The services also allow mapping in the backward direction, so that mappingService.getKey('vip', 'VIP') == 'Y'.
| Ambiguous mappings In case that a mapping definition maps more than one key to the same value (e.g. A->C and B->C), the backward mapping only contains the last entry, i.e. C->B. |
As of IPF 1.6, BidiMappingService also can be initialized using a list of mapping files:
<!-- Groovy class that provides the operations on the mappings --> <bean id="hl7MappingService" class="org.openehealth.ipf.modules.hl7.mappings.BidiMappingService"> <property name="mappingScripts"> <list> <value>classpath:example1.groovy</value> <value>classpath:example2.groovy</value> </list> </property> </bean>
Conflicting mappings are overridden by later list entries, i.e. mappings defined in example2.groovy override existing mappings defined in example1.groovy.
As of IPF 1.6, BidiMappingService also supports default reverse mappings, i.e. you can specify an ELSE mapping also from the reverse direction:
mappings = {
reverseMapping(
key : 'value',
(ELSE) : 'unknownKey',
'unknownValue' : (ELSE)
)
reverseMappingWithClosures(
key : 'value',
(ELSE) : 'unknownKey',
{ 'key' } : (ELSE)
)
}
The reverseMappingWithClosures mapping also demonstrates how to use a closure in order to return a default key that is already defined as key for a regular mapping.
API Extensions
Strings
The String class has been extended to facilitate usage of the Mapping Service. As an example, we assume the mappins defined as shown in the example.groovy definition in the Mapping Service subchapter.
String.map() maps the left to the right side. The identifier for the mapping can either be passed as argument:
assert 'E'.map('encounterType') == 'EMER' assert 'X'.map('encounterType') == null assert 'X'.map('encounterType', 'DEFAULT') == 'DEFAULT'
or as part of the method call. The methods are dynamically added based on the registered mapping identifiers in the defined MappingService instance.
assert 'E'.mapEncounterType() == 'EMER' assert 'X'.mapEncounterType() == null assert 'X'.mapEncounterType('DEFAULT') == 'DEFAULT'
If provided by the MappingService implementation, you can also map in the reverse direction:
assert 'EMER'.mapReverse('encounterType') == 'E' assert 'EMER'.mapReverseEncounterType() == 'E'
Code systems are often associated with a globally unique identifier, usually in form of an ISO Object Identifier (OID). The identifier of both sides of a mapping can be obtained as follows:
assert 'encounterType'.keySystem() == '2.16.840.1.113883.12.4' assert 'encounterType'.valueSystem() == '2.16.840.1.113883.5.4'
Finally, you can check whether a certain key or value is contained in the mapping.
assert 'encounterType'.hasKey('E') == true assert 'encounterType'.hasKey('X') == false assert 'encounterType'.hasValue('EMER') == true assert 'encounterType'.hasValue('XXXX') == false
HAPI Message
You can create positive or negative acknowledgments to messages. The acknowledgments are in the same HL7 version as the original message and is populated as specified in the parameters.
def ack = msg.ack()
def nak1 = msg.nak('Reason for failure')
def nak2 = msg.nak(new HL7Exception('reason for failure', 204)
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 (2.5 in the following example):
def nak = ca.uhn.hl7v2.model.Message.defaultNak(e, '2.5')
Generating acknowledgments is, however, only a special case of generating a response to an original message. If the response is defined as dedicated HL7 message, as with responses to Query messages, you have to use the respond(eventType, triggerEvent) extension. The MSH and MSA segments of the result message are populated as required by the HL7 specification.
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message
You can 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 }
The matches() method can be used e.g. inside closures for filters and routers in HL7 Camel routes like
...
.choice()
.when { it.in.body.matches('ADT','A01','2.5') }
.to("direct:handle_ADTA01")
.otherwise()
.to("direct:unknown_message")
...
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
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 is possible avoid using this in production environments:
println msg.dump()
HAPI Structures
All HAPI Structures (i.e. not only Messages, but also arbitrary Groups and Segments) can be printed by calling the encode() extension. 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
HAPI Types
All HAPI Types (i.e. Primitives, Composites, and Varies) can be printed by calling the encode() extension.
assert message.MSH.messageType.encode() == 'ADT^A01' // Together with the HL7 DSL, we can also write assert message.MSH[9].encode() == 'ADT^A01'
As described above with java.lang.String objects, the mapping extensions can be applied directly on all HAPI types. The encode() extension is called before the mapping is executed.
// 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 DSL, we 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'
Collection
A further common mapping scenario is having a collection of keys that map to a value or collection of values.
As an example, we assume the following mapping, which is registered under the name 'test':
| key | value |
|---|---|
| A~B~C | X~Y~Z |
| D~E~F | A |
To comply with the rules for Groovy maps, we better don't directly use Collection as keys; instead we use the tilde "~" to separate the collection elements in a single String. You can either use lists or tilde-encoded strings on the value side.
Collection.map() behaves exactly like String.map() shown above:
assert ['A','B','C'].map('test') == ['X','Y','Z'] assert ['A','B','C'].map('test')[0] == 'X' assert ['D','E','F'].map('test') == A assert ['X','Y','Z'].mapReverse('test') == ['A','B','C'] assert 'A'.mapReverse('test') == ['D','E','F']
Note that with the API you work with lists on the key and value side of the mapping.
Collections may also contains HAPI types, in this case Type.encode() is called on each list element before the mapping is executed.
HL7 Validation
Concept
The HAPI library already offers support for validating HL7 messages by definition of rules that check against constraints on type level, message level, and encoded message level. However, definition of these rules is rather cumbersome.
The IPF HL7 module adds support for specifying validation rules in a way that is easy to write and easy to understand. It facilitates the definition of custom validation rules by exploiting features of the Groovy language that is already used in other parts of IPF.
Validation basics
Creating a ValidationContext
Validation rules are defined by instantiating an implementation of the ValidationContext interface of HAPI. IPF comes with the org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext class, which primarily offers two benefits:
- It provides access to a Validation Builder that allows for definition of validation rules using a rather simple Domain Specific Language (DSL)
- It supports Validation rules that constrain their target object by evaluating Groovy closures for maximum flexibility.
import org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext ... DefaultValidationContext context = new DefaultValidationContext() ...
Add rules to the ValidationContext
Although it is possible to manually instantiate the Validation rule classes that come with IPF, you should use the Validation builder.
...
context.configure()
.forVersion('2.5') // following rule applies to HL7 v2.5
...
.forVersion('2.2 2.3 2.4') // following rule applies to HL7 v2.2, 2.3, and 2.4
...
.forVersion().asOf('2.3') // following rule applies to HL7 versions starting with 2.3
...
.forVersion().before('2.3') // following rule applies to HL7 versions older than 2.3
...
.forVersion().except('2.4') // following rule applies to HL7 versions but 2.4
...
.forAllVersions() // following rule applies to all HL7 versions
...
Before actually specifying the validation rule, its application is restricted to one or more HL7 versions. The possibilities are shown in the code example above and are self-describing.
Now the rules can be specified by specifying constraints on one or more of
- primitive type level
- message level
- encoding level
...
context.configure()
.forVersion('2.5') // limit to HL7 v2.5 messages
.type('DT') // constraints for the DT type
...
.message('ADT', 'A01') // constraints for ADT_A01 messages
...
.encoding() // constraints for encoded messages
...
Details on how the validation rules are syntactically defined are shown below in detail.
Reuse a ValidationContext
If there is already an instance of DefaultValidationContext, you can simply add further rules to it:
DefaultValidationContext context = new DefaultValidationContext() // Add a number of rules context.configure() .forVersion() ... // Add some more rules context.configure() .forVersion() ...
However, it is also possible to reuse any ValidationContext instance with a DefaultValidationContext.
DefaultValidationContext context = new DefaultValidationContext() // Use the builder to add Groovy-based validation rules context.configure() .forVersion() ... // Add existing ValidationContext instances context .addContext(additionalValidationContext1) .addContext(additionalValidationContext2) ...
Validate messages
There are two possibilities to execute validation rules
- during parsing
- after parsing
To validate during parsing, simply configure your Parser instance with the ValidationContext.
...
def parser = new PipeParser(context)
Message message = parser.parse(msgText)
...
During parsing, all variants of validation rules (i.e. primitive types, message and encoding; see below) are checked.
To validate after parsing, use the HAPI MessageValidator class
import ca.uhn.hl7v2.validation.MessageValidator ... new MessageValidator(context, true).validate(message)
You can achieve the same by using a bridge implementing the org.openehealth.ipf.commons.core.modules.api.Validator interface:
new HL7Validator().validate(message, context)
This is the variant that IPF's HL7 DSL extension also uses internally. Currently, only message rules are checked when using the HL7Validator.
Validation variants
Validating against primitive type constraints
HL7 Primitive types have no substructure, i.e. they directly contain values. The values are usually restricted by one or more of the following contraints:
- length
- value type (e.g. whether it evaluates to a decimal or number)
- regular expression pattern
Regular expressions can also be used to restrict length and type of the value, but regular expressions are often hard to read and understand.
Each HL7v2 version defines a set of primitive types; over the time the number of types have increased and/or the constraints have been modified.
A type constraint for a range of HL7 versions is defined as follows:
ValidationContext context = new DefaultValidationContext() context.configure() .forVersion().asOf('2.3') .type('DT') .matches(/(\d{4}([01]\d(\d{2})?)?)?/) // YYYY[MM[DD]] .withReference('Version 2.5 Section 2.A.21')
This enforces that all instances of type DT type must match a date pattern, where the year as mandatory and the month the day of month is optional. The example uses a regular expression to specify the date pattern. Please also read about regular expressions in Groovy.
The Validation DSL for primitive types supports the following constraints:
| Constraint type | Method | Example | constraints... |
|---|---|---|---|
| maximum length | .type('XXX').maxSize(int) | maxSize(255) | ... the maximum length to 255 characters |
| length range | .type('XXX')[min..max] | [10..20] | ... the length to be between 10 and 20 |
| existence | .type('XXX').notEmpty() | ... that there must be a value of >= 1 character | |
| matches | .type('XXX').matches(regexp) | matches(/(\d{4}([01]\d(\d{2})?)?)?/) | ... the value be formatted as a date |
| number type | .type('XXX').isNumber() | ... the value to be a decimal number | |
| user defined | .type('XXX').checkIf(Closure) | checkIf { it.size() <= 255 } | ... the maximum length to 255 characters |
The Closure syntax is the most flexible way to define constraints on a type. Internally, all other constraint methods are implemented by calling checkIf using a specific closure.
Additionally, primitive type constraints are "misused" in HAPI for trimming space characters from values. The Validation DSL supports this as well:
| Constraint type | Method | Example | function |
|---|---|---|---|
| trim | .type('XXX').omitLeadingWhitespace() | removes leading whitespace characters | |
| trim | .type('XXX').omitTrailingWhitespace() | removes trailing whitespace characters |
You can combine any of constraint methods to define more than one rule for a type:
...
.type('XXX').omitLeadingWhitespace().isNumber().checkIf { it > 50 && it < 100 }
...
HL7v2 defines constraints for each primitive types for each version. IPF comes with predefined validation rules that enforce these constraints. You can access them using
import ca.uhn.hl7v2.validation.ValidationContext import org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext ... ValidationContext context = DefaultTypeRulesValidationContext()
| PipeParser The PipeParser class in IPF is preconfigured with the default type rules. |
| When primitive type rules are checked Note that primitive type rules can only be applied by configuring the parser. Validation then is executed whenever a primitive value is set:
|
Validating against message constraints
By default, the HAPI parsers accept about any message as long as follows the HL7 syntax rules. For messages, groups and segments not defined in the respective HL7 standard, generic structures are instantiated internally. This allows parsing custom messages even without prior definition of Z segments or similar custom structures.
However, any piece of software with an HL7 interface needs to validate whether the received messages comply with either the official specification or specific for a certain customer or project.
| When message type rules are checked Message rules can be enforced by either configuring the parser or by manually validating a parsed message by using e.g. a HL7Validator. |
HL7 Conformance profiles
Conformance profiles have been introduced with HL7 2.5 as a standardized means of defining the static and dynamic properties of a HL7 message. Conformance profiles are encoded in XML and can be seen as a formal specification language. For more details on HL7 conformance profiles, please refer to chapters 2.12 and 2.19 of the HL7v2.5 specification document.
There are tools that facilitate the definition of conformance profiles, most notably the Messaging Workbench, which can be downloaded here.
HAPI supports checks against conformance profiles. In the IPF Validation framework it can be used as follows:
DefaultValidationContext context = new DefaultValidationContext() ProfileStoreFactory.setStore(new ClassPathProfileStore()) context.configure() .forVersion('2.5') .message('QBP', 'Q22').conformsToProfile('IHE-PDQ-QBP-Q22')
In this case, a file with the name IHE-PDQ-QBP-Q22.xml is looked up in the root of the Java classpath. Without the call to ProfileStoreFactory.setStore, the default HAPI ProfileStore is used, which looks into the <hapi.home>/profiles directory.
HL7 Abstract Syntax specification
HL7v2 defines an abstract message syntax (see HL7v2.5 specification, Chapter 2.13), that specifies which groups and segments are expected for a specific message type. Cardinality is indicated by using
- brackets ([...]) for optional groups or segments [0..1]
- braces ({...}) for repeatable groups or segments [1..*]
- a combination of both ({[...]} or [{...}]) for optional and repeatable groups or segments [0..*]
IPF provides support for checking a message instance against such an abstract message syntax definition. The corresponding rule is almost a copy of the Message Syntax, with a few differences:
- segment names are specified in quotes ('')
- group names are specified like function calls inside the cardinality indicators described above
- a choice of one segment from a group of segments is currently not supported.
The following comparison gives an example:
| HL7 Abstract Message Syntax definition | IPF Validation rule |
|---|---|
MSH
[ { SFT } ]
PATIENT_RESULT
PATIENT
{ [ PID
[ PD1 ]
[ { NTE } ]
[ { NK1 } ]
VISIT
[ PV1
[ PV2 ]
]
VISIT
]
PATIENT
ORDER_OBSERVATION
{ [ ORC ]
OBR
[ { NTE } ]
TIMING_QTY
[{ TQ1
[ { TQ2 } ]
}]
TIMING_QTY
[ CTD ]
OBSERVATION
[{ OBX
[ { NTE } ]
}]
OBSERVATION
[ { FT1 } ]
[ { CTI } ]
SPECIMEN
[{ SPM
[ { OBX } ]
}]
SPECIMEN
}
ORDER_OBSERVATION
}
PATIENT_RESULT
[ DSC ]
|
DefaultValidationContext context = new DefaultValidationContext()
context.configure()
.forVersion('2.4')
.message('ORU', 'R01').abstractSyntax(
'MSH',
[ { 'SFT' } ],
{PATIENT_RESULT(
[PATIENT(
'PID',
[ 'PD1' ],
[ { 'NTE' } ],
[ { 'NK1' } ],
[VISIT(
'PV1',
[ 'PV2' ]
)]
)],
{ORDER_OBSERVATION(
[ 'ORC' ],
'OBR',
[{ 'NTE' }],
[{TIMING_QTY(
'TQ1',
[{ 'TQ2' }]
)}],
[ 'CTD' ],
[{OBSERVATION(
'OBX',
[ { 'NTE' } ]
)}],
[{ 'FT1' }],
[{ 'CTI' }],
[{SPECIMEN(
'SPM',
[{ 'OBX' }]
)}]
)}
)},
[ 'DSC' ]
)
|
Note that the fields inside segments can neither be specified nor validated with the abstract message syntax.
Custom validation
You can use message rules also to program you own custom constraints on one or more trigger events. All there is to do is to write a checkIf closure that returns an array of HAPI {{ValidationException}}s. The array is empty, if validation passes.
...
context.configure().forAllVersions()
.message('ADT', 'A01')
.checkIf { msg ->
def validationExceptions = []
// validate and return an (empty) ValidationException array
return validationExceptions
}
.message('ADT', 'A01 A04 A08')
.checkIf { msg ->
// define constraints for all three trigger events
}
.message('ADT', ['A01', 'A04', 'A08'])
.checkIf { msg ->
// same as above but specified as list
}
.message('ADT', '*')
.checkIf { msg ->
// for all trigger events of the ADT message type
}
Validating against encoded messages
These kind of rules can be used to validate the encoded representation of a HL7 message, i.e. their String representation in either ER7 (Pipe) or XML encoding. Besides providing a ClosureEncodingRule class, only a link to HAPI's XMLSchemaRule is defined:
...
.forVersion()
...
.encoding("XML").isValidXML()
...
Settings up validation rules using a Spring application context
To initialize a DefaultValidationContext from a Spring Framework application context, you need a ValidationContextFactoryBean and one or more ValidationContextBuilder beans.
<beans> ... <bean id="parser" class="org.openehealth.ipf.modules.hl7.parser.PipeParser"> <property name="validationContext" ref="context"/> </bean> <bean id="context" class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/> <bean id="defaultTypeRules" class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/> <bean id="myCustomRules" class="com.my.company.MyValidationContextBuilder"/> ... </beans>
Each ValidationContextBuilder will contribute to the overall set of rules being applied. In the example above, two rule sets are added
- the default set of primitive type rules
- A custom set of rules of either variant (type, message or encoding).
A skeleton implementation for such a custom builder is given in the following example:
import ca.uhn.hl7v2.validation.ValidationContext import ca.uhn.hl7v2.validation.ValidationException import org.openehealth.ipf.modules.hl7.validation.builder.RuleBuilder import org.openehealth.ipf.modules.hl7.validation.builder.ValidationContextBuilder public class MyValidationContextBuilder extends ValidationContextBuilder { public RuleBuilder forContext(ValidationContext context) { context.configure() .forVersion('...') .message(...) ... .type(...) ... .encoding(...) ... } }
DSL extensions
This section describes DSL extensions provided by the platform-camel-hl7 component. For a description of Camel-independent HL7 message processing features visit the pages HAPI DSL, HAPI extensions and HL7 validation. |
HL7 DSL extensions are defined in the org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension.groovy class. Their main purpose is to make HAPI DSL, HAPI extensions and HL7 validation features available in Camel routes. Extensions provided by this class may well be combined with other extensions that comply with the DSL extension mechanism.
HL7 adapter (un)marshalling
The ghl7() DSL extension allows you to convert between HL7 message strings (or streams) and org.openehealth.ipf.modules.hl7dsl.MessageAdapter objects (the MessageAdapter class implements the HAPI DSL). For example, to unmarshal a message adapter from a string (or stream) use
// ...
from('...')
.unmarshal().ghl7()
.to('...')
// ...
in your Groovy route definitions. To marshal a message adapter to an output stream use
// ...
from('...')
.marshal().ghl7()
.to('...')
// ...
(Un)marshaling options
HL7 message adapter unmarshalling and marshalling can be customized in the following ways. You can define
- a custom character set via the ghl7(java.lang.String charset) parameter,
- a custom HAPI parser via the ghl7(ca.uhn.hl7v2.parser.Parser parser) parameter
- or both via ghl7(ca.uhn.hl7v2.parser.Parser parser, java.lang.String charset)
The charset parameter is used to define the character set used for reading from and writing to a byte stream. The parser parameter allows you to define a custom HAPI parser when you unmarshal a message adapter from a stream. The message adapter adapts a HAPI message which in turn is created by a HAPI parser.
Here's an example:
def ca.uhn.hl7v2.parser.Parser parser = new MyCustomParser()
// ...
.unmarshal().ghl7(parser, 'ISO-8859-1')
// ...
.marshal().ghl7('ISO-8859-1')
//
HL7 message validation
HL7 messages can be validated in routes with the validate().ghl7() extension. If you don't want to use a default validation context you can provide one via the staticProfile() extension. Custom validation contexts can be created as described in HL7 validation. Here's an example:
DefaultValidationContext context = ... // create and configure a custom HL7 validation context.
// route 1
// ...
.unmarshal().ghl7()
.validate().ghl7() // HL7 message validation with default validation context
// ...
// route 2
// ...
.unmarshal().ghl7()
.validate().ghl7().staticProfile(context) // HL7 message validation with custom validation context
// ...
The HL7 message validation DSL relies on org.openehealth.ipf.modules.hl7dsl.MessageAdapter message bodies. These are created via unmarshal().ghl7 from input streams or strings. If you want to create a message validation context from an org.apache.camel.Exchange you can use the profile() DSL extension which defines an org.apache.camel.Expression or a Groovy closure as parameter.
org.apache.camel.Expression contextExpression = ...
// route 1
// ...
.unmarshal().ghl7()
.validate().ghl7().profile(contextExpression) // Validation context created by an expression object
// ...
// route 2
// ...
.unmarshal().ghl7()
.validate().ghl7().profile {exchange -> // Validation context created by a closure
// obtain or create validation
// context from message exchange
// and return it
}
// ...
| Backwards compatibility Earlier IPF versions allowed to set static profiles with the profile() extension. This is still possible but it is recommended to use staticProfile() instead. However for profile expressions you should use profile(). |
Example
Usually, for processing HL7 messages you might first want to unmarshal a message adapter from an input stream, validate the message using either a default or custom validation context and then use the HAPI DSL (provided by the message adapter) in processors, transmogrifiers or content based routers. Then the processing result is marshalled again for being transported to another endpoint. Here's an example:
from("direct:input1") // create a message adapter from an HL7 string .unmarshal().ghl7() // validate the message using a default validation context .validate().ghl7() // transmogrifiers are passed in-message bodies // and message headers by default. .transmogrify { msg, headers -> // set the MSH[5] field to whatever is // contained in the foo message header // (using the HAPI DSL) msg.MSH[5] = headers.foo msg } .choice() // when-closures are passed messages // exchanges by default. Here we make // routing decisions based in the MSH[5] // field value of the HL7 message (using // the HAPI DSL) .when { it.in.body.MSH[5].value == 'blah' } .marshal().ghl7() // adapter -> string .to('mock:output1') .when { it.in.body.MSH[5].value == 'blub' } .marshal().ghl7() // adapter -> string .to('mock:output2')