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.commons.map.MappingService interface deals with the requirement that processing 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 commons-map component extends the java.lang.String class to map between values. The modules-hl7 component additionally extends 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.commons.map.BidiMappingService"> <property name="mappingScript" value="classpath:example.map"/> </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. |
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.commons.map.BidiMappingService"> <property name="mappingScripts"> <list> <value>classpath:example1.map</value> <value>classpath:example2.map</value> </list> </property> </bean>
Conflicting mappings are overridden by later list entries, i.e. mappings defined in example2.map override existing mappings defined in example1.map.
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 mappings defined as shown in the example.map 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.