HL7 processing

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:

context.xml
<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

pom.xml for standalone use
<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.

pom.xml for use with Camel
<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:

pom.xml for using HL7 version 2.5 and 2.5.1
<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>
Example:
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)
Example:
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)
Example:
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)
Example:
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)
Example:
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
Example:
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
Copy composite or primitive by symbolic field name or index:
 segment['<symbolicFieldName>'] = composite
 segment['<symbolicFieldName>'] = primitive
 segment[i] = composite
 segment[i] = primitive
Example:
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
Copy component or primitive by index:
 composite[i] = component // non-primitive
 composite[i] = primitive
Example:
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

pom.xml for standalone use
<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:

pom.xml for use with Camel
<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:

pom.xml for using HL7 version 2.5 and 2.5.1
<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:

  1. com.mycompany.profile1.hl7def.v25.message
  2. com.mycompany.profile2.hl7def.v25.message
  3. 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:

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

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

  1. It provides access to a Validation Builder that allows for definition of validation rules using a rather simple Domain Specific Language (DSL)
  2. 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:

  • during parsing
  • when individual fields are modified afterwards, e.g. by while using a transmogrifier.

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.
Message rules are not enforced each time a part of the message is modified.

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

  1. the default set of primitive type rules
  2. 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

Unmarshal message adapter
    // ...
    from('...')
    .unmarshal().ghl7()
    .to('...')
    // ...

in your Groovy route definitions. To marshal a message adapter to an output stream use

Marshal message adapter
    // ...
    from('...')
    .marshal().ghl7()
    .to('...')
    // ...

(Un)marshaling options

HL7 message adapter unmarshalling and marshalling can be customized in the following ways. You can define

  1. a custom character set via the ghl7(java.lang.String charset) parameter,
  2. a custom HAPI parser via the ghl7(ca.uhn.hl7v2.parser.Parser parser) parameter
  3. 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:

(Un)marshaling options
    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:

Message validation with static profiles
    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.

Message validation with profile expressions

    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:

HL7 DSL 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')
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.