Introduction to the Open eHealth Integration Platform
Join the DZone community and get the full member experience.
Join For FreeThe Open eHealth Integration Platform (IPF) is an extension of the Apache Camel routing and mediation engine and comes with comprehensive support for message processing and connecting information systems in the healthcare sector. It is available under the Apache License version 2.0. IPF and Camel both focus on a domain-specific language (DSL) to implement and combine Enterprise Integration Patterns (EIPs) in integration solutions. IPF leverages the Groovy programming language for application development and for extending the Apache Camel DSL.
One example of a healthcare-related use case of IPF is the implementation of interfaces for transactions specified in IHE profiles. The use of IPF to implement actor interfaces for the IHE PIX and PDQ profiles was tested successfully at the 2009 Connect-a-thon. The actual actors were proprietary systems that were IHE-enabled via IPF-based integration solutions. It is important to note that usage of IPF is not just limited to the healthcare domain. It can also be used to build integration solutions for several other domains. However, this article will focus primarily on the healthcare-specific features of IPF.
Riding the Camel
IPF is built on top of the Apache Camel routing and mediation engine. This section deals briefly with the concepts behind Camel. For a more detailed overview, read Jonathan Anstey's excellent introductory article Apache Camel: Integration Nirvana.
Apache Camel is about Enterprise Integration Patterns (EIPs). EIPs have previously been described by Gregor Hohpe and Bobby Woolf in their book Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. They represent experiences of integration architects and can be used as building blocks for designing integration solutions. These patterns were published as recommendations without implying a concrete implementation. This is where Apache Camel comes in. Camel was created to provide implementations for EIPs. With Apache Camel, integration solutions are developed by combining integration patterns using a domain-specific language (DSL). In addition to EIP implementations, Camel also provides more than 60 components for connecting to a great variety of transports and APIs.
The four major concepts in Camel are components, endpoints, processors and the domain-specific language (DSL). Components provide connectivity to other systems. From components, endpoints are created for sending and receiving messages. Endpoints are uniquely identified by their URIs. Processors are used to route and transform messages between endpoints. To wire endpoints and processors together, Camel provides a DSL for Java. Consider a simple example:
from("jms:queue:validated")
.filter().xpath("/person[@name='Martin']")
.to("http://www.example.com/camel");
In this route definition, messages are read from a JMS queue (identified by the jms:queue:validated endpoint URI) and sent through a filter. The filter criterion is an XPath predicate. Any message that passes the filter will be POSTed to the http://www.example.com/camel endpoint. Camel also supports a Spring-based XML configuration for creating message processing routes (not shown here).
Riding IPF
Any feature provided by Camel can be used in combination with any other features provided by IPF. IPF makes its features available via extensions to the Camel DSL. These extensions have been implemented via Groovy meta-programming. For these extensions to be used in an IPF application, route definitions must be written in Groovy. Here is an example of a route definition that uses IPF's DSL extensions:
from('file:input') // read HL7 message from file
.unmarshal().ghl7() // create HL7 message object
.validate().ghl7() // validate HL7 message object
.filter {exchange -> // filter message using criteria defined by closure
def msg = exchange.in.body // use exchange.in.body to access HL7 message object
msg.PID[8].value == 'F' // use HL7 DSL to access and compare PID-8 field value
}
.to('http://...') // send message to destination (via HTTP
In this route, an HL7 message is read from a file, validated and then filtered based on the value of the 8th field in the PID segment. To obtain this field value, the filter closure uses IPF's HL7 DSL. This DSL is explained in detail in the next section. If the message passes the filter it is forwarded to its destination via HTTP. The route definition uses the DSL elements from, unmarshal, filter and to from Camel directly, all other DSL elements are extensions provided by IPF, including the use of closures for filter criteria.
IPF DSL extensions can be used as if they are native Camel DSL elements. Furthermore, the DSL used in applications is not just limited to the DSL provided by Camel and IPF. Application-specific DSL extensions can also be written using IPF's DSL extension mechanism. This mechanism also supports modularization of DSL definitions. Any component can contribute its own DSL extensions. Consequently, the scope of the overall DSL depends on which components were deployed for the application. Inside an OSGi environment, IPF can even automatically detect and activate DSL extensions provided by different bundles.
The following sections will explore the healthcare-specific features of IPF in greater detail. For a detailed description of all IPF features consult the IPF reference documentation. For a more hands-on guide on how to write IPF applications, the IPF tutorials are a good starting point.
HL7 message processing
HL7 (Health Level 7) is an ANSI-accredited standards developing organization (SDO) in the healthcare sector. Among other standards it focuses on developing messaging standards for the clinical and administrative domain. This standard is often referred to as the HL7 messaging standard. There are several versions of this standard. HL7 version 2.x is widely used, the most recent update was version 2.6 in 2007. It differs significantly from the XML-based HL7 version 3. The following subsections focus on IPF's support for HL7 version 2.x. Support for HL7 version 3 is currently under development. IPF uses the open source HAPI library as its basis for HL7 version 2 message processing.
HL7 version 2 at a glance HL7 version 2 messages have a hierarchical structure and are organized into groups, segments, fields and datatypes which can also repeat. Message elements are referenced by their path within the hierarchy (see navigation syntax in the following figure). |
HL7 DSL
The core of IPF's HL7 message processing capabilities is the HL7 DSL. This is a domain-specific language for accessing and manipulating HL7 version 2 messages. The IPF library that implements this DSL (modules-hl7dsl) can also be used standalone (i.e. independent from other IPF components) in any Groovy application. Seamless integration into the IPF DSL is provided as well. The following examples presume a certain degree of familiarity with HL7 version 2 message structures and terms.
Constructing messages
The entry point to the HL7 DSL is IPF's MessageAdapter. To create a MessageAdapter instance from an HL7 file we use the load method from the MessageAdapters utility class.
import org.openehealth.ipf.modules.hl7dsl.MessageAdapter
import org.openehealth.ipf.modules.hl7dsl.MessageAdapters
MessageAdapter message = MessageAdapters.load('ADT-A01.hl7')
This loads the HL7 file ADT-A01.hl7 from the classpath.
Accessing message content
The following example obtains the PID segment contained in the PATIENT group which itself is contained in the first repetition of the repeatable PATIENT_RESULT group.
// repeating elements are indexed from 0 to n
def segment = message.PATIENT_RESULT(0).PATIENT.PID
The function call operator () is used to refer to an element in a repetition. The element index is passed as argument. Obtaining fields is similar to obtaining groups and segments except that fields are often referred to by index rather than by name. To obtain the MSH-3 field from a message we can write:
// fields are indexed from 1 to n
def composite = message.MSH[3]
If the field is a composite we access the second component with:
// components of a composite field are indexed from 1 to n
def primitive = message.MSH[3][2]
or, equivalently,
def primitive = message.MSH.sendingApplication.universalIDType
As shown above, navigation is also possible using field names instead of indices. Care must be taken, because along with the change of internal message structures, individual field names change between HL7 versions, even when they refer to the same position of the field in a segment. If the version of the HL7 message is not known in advance, it is better to use the more concise index notation.
Fields may also repeat. To obtain a given element of a repeating field the function call operator () is used, just like with groups and segments. In the next example we obtain the first element of the repeating NK1-5 field. Because NK1 is a repeating segment, the function call operator is used on segment-level, too.
def field = message.NK1(0)[5](0)
def fieldList = message.NK1(0)[5]()
Omitting the repetition index returns a list of repeating elements. Omitting the function call operator, the first repetition of a group, segment or field is assumed. This is called smart navigation:
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
Using smart navigation, the expressions are usually shorter and less error-prone. Moreover, in many cases the same expression can be used for different HL7 versions, making the DSL more portable.
Modifying message content
A segment of one message can be assigned the segment value of another message using the assigment operator (=). The following example copies the EVN segment from message2 to message1.
message1.EVN = message2.EVN
To change a field value we navigate to that field (either by name or index, as shown above) and assign it a string or another field value.
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. In the following example we copy the composite NK1(0)[4] field from message2 to message1.
message1.NK1(0)[4] = message2.NK1(0)[4]
Externalizing messages
The left-shift operator (<<) appends the string-represenation of a MessageAdapter object to a writer. In the following example, we write an HL7 message to System.out.
MessageAdapter message = ...
System.out << message
To obtain the string-representation directly, we can use the MessageAdapter.toString() method.
MessageAdapter message = ...
def rendered = message.toString()
Usage in route definitions
This section shows a few examples how to combine the HL7 DSL with IPF's route definition DSL. The unmarshal().ghl7() extension should be used to create a MessageAdapter object from an external HL7 message representation. The created MessageAdapter object can then be used in subsequent processors.
from('file:input')
.unmarshal().ghl7()
.process {exchange ->
MessageAdapter message = exchange.in.body
...
}
...
The reverse operation marshal().ghl7() marshals a MessageAdapter object into a stream. This is often needed for transmitting HL7 messages over a variety of transports such as JMS, for example.
from('file:input')
.unmarshal().ghl7()
...
.marshal().ghl7()
.to(jms:queue:validated)
Another usage example is content-based routing. In the following example a message is routed to different destinations depending on the content of the MSH[4] field. Here we use closures in combination with Camel's when DSL element for implementing routing rules. The it variable inside closures represents a message exchange.
from(...)
.unmarshal().ghl7()
...
.choice()
.when { it.in.body.MSH[4].value == 'ABC' }
.to(...)
.when { it.in.body.MSH[4].value == 'DEF' }
.to(...)
.otherwise()
.to(...)
HL7 validation
IPF also adds support for specifying validation rules in a way that is easy to write and simple to understand. It facilitates the definition of custom validation rules by providing a dedicated validation DSL. Like the HL7 DSL, the validation DSL can also be used standalone but also integrates well into the IPF DSL for defining message processing routes. The IPF component that implements HL7 validation is modules-hl7. Validation rules are defined by extending the ValidationContextBuilder class of IPF. The validation rules DSL is provided by the RuleBuilder class. The following example defines a subset of segments from the HL7 version 2.2 specification.
package example
import ca.uhn.hl7v2.validation.ValidationContext
import org.openehealth.ipf.modules.hl7.validation.builder.RuleBuilder
import org.openehealth.ipf.modules.hl7.validation.builder.ValidationContextBuilder
class SampleRulesBuilder extends ValidationContextBuilder {
RuleBuilder forContext(ValidationContext context) {
new RuleBuilder(context)
.forVersion('2.2')
.message('ADT', 'A01').abstractSyntax(
'MSH',
'EVN',
'PID',
[ { 'NK1' } ],
'PV1',
[ { INSURANCE(
'IN1',
[ 'IN2' ] ,
[ 'IN3' ]
)}]
)
}
}
The sequence and cardinality of groups and segments is defined in a syntax that is very closely related to the HL7 Abstract Message Syntax. The message must contain the segments MSH, EVN, PID and PV1; it may contain zero or more NK1 segments and it may contain a repeatable INSURANCE group. In a next step we configure our SampleRulesBuilder in a Spring application context along with a ValidationContextFactoryBean.
<bean id="validationContext"
class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean">
</bean>
<bean id="customRules"
class="example.SampleRulesBuilder">
</bean>
The ValidationContextFactoryBean auto-detects any beans of type ValidationContextBuilder and adds their validation rules to a ValidationContext that is used in route definitions. The validate().ghl7() DSL extension, which we have already seen, can be configured with a custom validation profile using the profile() DSL extension. The validation context is looked up from the Spring application context via bean(ValidationContext.class).
import ca.uhn.hl7v2.validation.ValidationContext
import org.apache.camel.spring.SpringRouteBuilder
class SampleRouteBuilder extends SpringRouteBuilder {
void configure() {
from(...)
.unmarshal().ghl7()
.validate().ghl7().profile(bean(ValidationContext.class))
...
.to(...)
}
}
Code mapping
HL7 message processing often involves mapping between code systems i.e. from one set of codes into a corresponding different set of codes. For example, HL7 version 2 and HL7 version 3 use different code systems for most coded values such as message type, gender, clinical encounter type, marital status codes, address and telecommunication use codes, just to mention a few. IPF defines a mapping service that provides the mapping logic. This may be a simple map but it can also be a facade to a remote mapping or terminology service. IPF's default mapping service implementation, the BidiMappingService, supports bidirectional mappings and reads custom mapping definitions from mapping files. An instance of the BidiMappingService can be created with the following bean definition.
<bean id="mappingService" class="org.openehealth.ipf.modules.hl7.mappings.BidiMappingService">
<property name="mappingScript" value="classpath:example.map">
</bean>
The mapping service references a mapping file example.map on the classpath. If there is more than one mapping file, a list can be provided via the mappingScripts property. Here is the content of example.map:
mappings = {
encounterType(['2.16.840.1.113883.12.4','2.16.840.1.113883.5.4'],
E : 'EMER',
I : 'IMP',
O : 'AMB'
)
}
The example mapping file is a Groovy script and contains a single mapping with three entries for encounter type codes. Also defined are the ISO Object Identifiers (OIDs) for the key and value code systems. The mapping service can now be accessed either directly or via methods on java.lang.String. The String.map() method maps the codes on the left side 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 a method name.
assert 'E'.mapEncounterType() == 'EMER'
assert 'X'.mapEncounterType() == null
assert 'X'.mapEncounterType('DEFAULT') == 'DEFAULT'
A dynamic dispatch is used to select the mapping definition from the method name. The method names must therefore correspond to the registered mappings. Mapping in the reverse direction is equally possible.
assert 'EMER'.mapReverse('encounterType') == 'E'
assert 'EMER'.mapReverseEncounterType() == 'E'
Code systems are often associated with a globally unique identifier, usually in form of an 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'
Code mapping methods may also be used in combination with the HL7 DSL.
MessageAdapter message = ...
assert message.PV1.patientClass.value == 'I'
assert message.PV1.patientClass.map('encounterType') == 'IMP'
assert message.PV1.patientClass.mapEncounterType() == 'IMP'
Response messages
HL7 messaging often requires the return of an HL7 response message to the sender. With IPF, positive (ACK) or negative (NAK) acknowledgments to messages can be generated. Acknowledgments are in the same HL7 version as the original message and are populated with arguments to the ack() method.
MessageAdapter message = ...
def ack = message.ack()
def nak1 = message.nak('Reason for failure')
def nak2 = message.nak(new HL7Exception('Reason for failure', 204))
Generating acknowledgments is, however, only one special case of generating a response to an original message. For responses other than acknowledgements, a response message prototype can be created via the respond(eventType, triggerEvent) method. The MSH and MSA segments of the response message are then populated as required by the HL7 specification.
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message
More features
So far, the main focus has been on HL7 message processing. IPF has many more features and services to support the development and the operation of production-quality integration solutions. Some of them are briefly described in the following list. A detailed description would far exceed the scope of this paper. For a complete overview refer to the IPF reference documentation.
- Core features. Collection of domain-neutral message processors and DSL extensions usable for general-purpose message processing including support for Groovy XML processing and support for using closures with Camel DSL elements.
- OSGi support. Support for running IPF and its services inside an OSGi environment and for the development of OSGi-ready IPF applications.
- Flow management. A service for monitoring and managing message flows through IPF applications. The flow manager also supports a replay of messages for e.g. recovery from failures.
- Large message support. Allows for memory-efficient processing of large messages.
- Event infrastructure. An infrastructure for publishing and consuming system and application events. Can be used e.g. for separating logging, audit or statistics concerns from application-specific route definitions. Can also be used to integrate with complex event processing (CEP) engines.
Outlook
Upcoming IPF releases will provide support for CDA (Clinical Document Architecture) and IHE (Integrating the Healthcare Enterprise). CDA is an XML-based document markup standard that specifies the structure and semantics of a clinical document for the purpose of exchange. IHE is an initiative of healthcare professionals and industry to improve the way computer systems in the healthcare sector share information. The goal of IPF is to make it as easy as possible for developers to implement the CDA, IHE (and other clinical) standards in their applications. Here are a few examples of what to expect in the next IPF release.
CDA support
IPF's CDA support will focus on building CDA documents using a domain-specific language. This DSL supports the creation of structurally correct CDA documents by enforcing CDA-relevant schema definitions but without dealing with low-level XML details. The DSL is implemented by a custom Groovy builder, the CDABuilder. In the following code snippet we use the CDABuilder to create a CDA document using CDA-specific terms such as clinicalDocument, code, title, recordTarget and so on. For printing the created document to stdout we use the left-shift (<<) operator.
// Create a CDA builder
CDABuilder builder = new CDABuilder()
// Create a new CDA document
def document = builder.build {
clinicalDocument {
id(root:'2.16.840.1.113883.19.4', extension:'c266')
code(
code:'11488-4',
codeSystem:'2.16.840.1.113883.6.1',
codeSystemName:'LOINC',
displayName:'Consultation note'
)
title('Good Health Clinic Consultation Note')
recordTarget {
patientRole {
id {
extension="12345"
root="2.16.840.1.113883.19.5"
}
patient {
name {
given('John')
family('Doe')
}
birthTime('19320924')
}
//...
}
//...
}
//...
}
//...
}
// Write document XML to stdout
System.out << document
CDA support will also include support for selected CDA profiles from IHE, HL7/ASTM and HITSP specifications, for example XPHR and CCD. Profile-specific CDA DSL extensions will enforce the constraints imposed by the profile specifications. As an example, predefined CDA sections could then be added without knowing their templateID-OIDs or their exact nested XML structure. DSL support for parsing, validating, transforming and rendering CDA documents will complete the feature set.
IHE support
IPF's IHE support is a framework for creating actor interfaces as specified in IHE profiles. Most likely, support for the XDS profile will be the first to come. XDS stands for Cross-Enterprise Document Sharing and deals with registration and distribution of, and access to clinical documents across health enterprises. Central to this profile are the actors document registry and document repository. A number of document management systems could in principle act as registry and/or repository in the XDS profile. Most of these, however, do not support the XDS actor interface specifications right out of the box. This is were IPF comes in. It helps developers to build IHE actor interfaces for existing information systems. Consider this example:
from('ihe:xds.b:iti-41?port=8080')
.process { exchange ->
def document = exchange.in.body
// do further document processing here ...
}
// communicate with your document management system
.to('http://...')
// notify about availability of new document
.to('ihe:nav:iti-25:?to=martin@openehealth.org')
This route starts a server to receive documents according to the ITI-41 transaction of the XDS.b IHE profile. ITI-41 is the Provide and Register Document Set transaction in XDS.b. XDS.b requires documents to be transported via SOAP and ebXML standards. To free developers from having to deal with low-level SOAP/ebXML handling, these communication details are hidden inside an ihe component (eventually there may be more than one). Subsequent processors can access the transported document without having to deal with ITI-41 details. After processing, the incoming document is uploaded to a document management system and, finally, martin@openehealth.org is notified about the availability of a new document. Notifications are sent according to the IHE NAV profile where NAV stands for Notification of Document Availability. This example is of course oversimplified (for instance it does not address responses, etc) but it still gives an idea of the abstraction level on which IHE interfaces can be implemented for existing systems.
Conclusion
Apache Camel is a good answer to many of today's integration problems. It provides a DSL for implementing Enterprise Integration Patterns and offers developers a simple and efficient way to deal with the diversity of applications and transports in distributed systems. IPF brings the power of Apache Camel to the healtcare domain and makes healthcare IT standards usable by means of a domain-specific language that closely resembles the language of domain experts. The DSL extension mechanism permits the evolution of even more specialized healthcare DSLs. IPF's support for DSL modularization makes these DSLs and their implementing components reusable in different integration scenarios.
Author
Martin Krasser is a software architect and engineer working for InterComponentWare AG. He focuses on distributed systems, application integration and application security. Martin is the founder and project lead of the open source IPF project.
Opinions expressed by DZone contributors are their own.
Comments