IPF reference - single

Introduction

Getting started

Prerequisites

The Open eHealth Integration Platform (IPF) is based on Apache Camel. It is therefore important that you have a good understanding how Camel works before continuing with the IPF documentation. For a general Camel overview go the project's main page. Camel tutorials can be found on a separate tutorials page. A recommended starting point is the tutorial introducing Camel steadily in a real life integration.

Application development on the Open eHealth Integration Platform (IPF) follows Enterprise Integration Patterns. Integration platforms or service buses won't help you unless you have a good understanding of the principles and concepts of messaging solutions. This book gives you excellent hints how to design messaging-based integration solutions.

Initial reading

After you've learned some basics about Apache Camel the best way to start with IPF is to read through the IPF overview section. It will give you a high-level view of the features provided by IPF including some links to more detailed documentation. The IPF architecture section starts with diagrams of the physical components that make up IPF. A short description of the component namspaces and the individual components is given as well.

Infrastructure setup

Before you start working with IPF make sure that you've read the IPF development pages. These will explain how to

First project

The first steps tutorial is a good starting point for creating your first IPF project. It demonstrates how to use IPF project archetypes for creating a project and walks through the key project artifacts. A simple message processing example will introduce some core features of IPF. You can find further tutorials on the IPF tutorials page.

IPF overview

The Open eHealth Integration Platform (IPF) is an extension of the Apache Camel routing and mediation engine. It has an application programming layer based on the Groovy programming language and comes with comprehensive support for message processing and connecting systems in the eHealth domain. IPF provides domain-specific languages (DSLs) for implementing Enterprise Integration Patterns in general-purpose as well as healthcare-specific integration solutions. These DSLs are extensible via Groovy meta-programming. An example of an healthcare-related use case of IPF is the implementation of interfaces for transactions specified in IHE profiles, but you may also use it for developing integration solutions in other domains. IPF can be easily embedded into any Java application and additionally supports deployments inside OSGi environments. Failure recovery and high-availability features support application developers implementing non-functional requirements. The following table summarizes the IPF features.

Feature Description
Apache Camel IPF is based on Apache Camel. For an overview of Camel's rich feature set (which can be fully used in IPF applications) refer to the project's integration patterns and integration components pages.
Groovy scripting layer With IPF you define integration routes with the Groovy programming language. It is more than a mere usage of Camel's domain-specific language (internal DSL or fluent API) inside Groovy: Camel's native DSL has been extended to support e.g. the usage of closures (for inline definitions of message processors, routing rules etc.) and also provides a DSL extension mechanism to define custom extensions to the Camel DSL.
DSL extension mechanism The DSL extension mechanism is a Groovy meta-programming-based mechanism for defining new DSL elements to be used in integration routes. This is especially useful if you want to provide custom language elements for re-occurring message processing patterns or if you want to design a project-specific message processing DSL (e.g. one that is related to the HL7 domain).
DSL extension index An index of all predefined DSL extensions provided by IPF.
Core features These are domain-neutral message processors and DSL extensions usable for general-purpose message processing. The core features also enhance existing Camel DSL elements for usage with Groovy-specific language elements such as closures. For XML message processing there is special Groovy XML support.
HL7 message processing Basis for HL7 message processing is the HL7 DSL, the HAPI extensions and the HL7 validation DSL. These provides the basis for implementing HL7 message processing routes.
IHE support A set of components for creating actor interfaces as specified in IHE profiles. IPF currently supports creation of actor interfaces for the IHE profiles XDS.a, XDS.b, PIX and PDQ.
CDA support A domain-specific language for building and navigating CDA documents. This DSL supports the creation of structurally correct CDA documents by enforcing CDA-relevant schema definitions but without dealing with low-level XML details.
Flow management A platform service to monitor, query, audit, replay and cleanup message flows. The management interfaces are based on JMX.
OSGi support Enables the deployment of IPF components (bundles) to OSGi platforms. IPF service bundles register platform services at the OSGi service registry for consumption by IPF applications. Extender bundles control the activation of DSL extensions inside an OSGi environment. A reference implementation of IPF on top of Eclipse Equinox is available as IPF runtime in the IPF Tools project. This project also maintains the complete IPF OSGi documentation.
Event infrastructure An infrastructure for unified publishing of system-events and application-events. Subscriber components can be configured to translate application events to e.g. Atom/RSS feeds or log files to mention a few.
Performance measurement DSL and tools to determine the performance characteristics of IPF applications. These allow for measuring the processing time of messages for routes or route parts as well as the message throughput. Performance measurement results can be viewed with a web browser.
Large message support Provides memory efficient processing of messages with large content sizes.
Quality of service IPF provides extensions, guidance and solution blueprints (code examples) for implementing non-functional requirements. Covered topics are transactional messaging, flow management, load-balaning and high-availability.
Module adapters An infrastructure for including platform-independent message processing libraries into platform-specific message processing routes. An alternative is Camel's bean integration mechanism.
Tutorials A bunch of tutorials that help you get started with IPF.
Guidelines Guidelines for IPF application development. For example, the DSL extensions guide describes how to write you own DSL extensions.
Project templates Maven archetypes for most commonly used IPF project types, ranging from simple embedded integration solutions to cluster configurations supporting high-availability scenarios. Usage examples of IPF features are provided as well.

IPF subprojects

Since version 2.0 of IPF several existing components as well as newly developed components have been moved to IPF subprojects. There are currently two IPF subprojects which are described in the following subsections.

IPF Tools

This project provides Eclipse-based development, administration and monitoring tools for the Open eHealth Integration Platform.

  • The development tools support developers in creating, testing and packaging IPF applications within the Eclipse plugin development environment (PDE). IPF applications created with these tools can be deployed and operated on OSGi R4.1 platforms. Part of the development tools is the IPF Runtime - the OSGi edition of IPF.
  • The administration and monitoring tools are Eclipse plugins for managing IPF platform and application resources. Examples are JMX-based system management clients or the IPF platform manager.
  • The IPF runtime is an Eclipse plugin that make IPF features and services available for application development inside Eclipse. The runtime is used to develop OSGi-based IPF applications.

For guidelines how to contribute to this project refer to the Contributing page of the IPF documentation.

IPF Lab

This project is the incubation area for the projects Open eHealth Integration Platform and Open eHealth Integration Platform Tools. Contributions of new features from the IPF community will be mainly done in this project. New features may come from existing IPF applications or are developed from scratch directly in the IPF Lab. This project also hosts components from current research activities such as integrating IPF into cloud-computing environments or into different web application frameworks. For guidelines how to contribute to this project refer to the Contributing page of the IPF documentation.

IPF architecture

IPF architecture in OSGi environments described elsewhere

IPF's architecture in OSGi environments is described in the architecture section of the OSGi support page. It complements the information presented here.

Component architecture

The following figure gives an overview of the IPF components, their dependencies and their namespaces (abbreviated package names). The components of the ihe subpackages (white color) are shown in the next section. The last subsection describes the namespaces and components in more detail. The component names match the jar file names in the Maven repository. The IPF component architecture is also closely related to the project structure in the code repository.

IHE components

The next figure shows the IPF IHE components. The Camel-independent components are contained in the ipf.commons.ihe package, Camel-specific IHE components are contained in the ipf.platform-camel.ihe package. IPF IHE components can be used to implement actor interfaces of IHE transactions. The transaction numbers (ITI) contained in the component name correspond to the transaction numbers in IHE specifications.

IPF and OSGi

All IPF components have been designed to run on an OSGi R4 version 4.1 platform. The osgi package additionally containes specialized OSGi bundles such as extender bundles and configuration fragments (not shown). For a detailed description about IPF and OSGi refer to the OSGi support section.

Namespace and component descriptions

Namespace Description
commons Namespace for commonly used libraries. Components in this namespace are independent of Apache Camel and have no dependencies to platform-camel.
modules Namespace for domain-specific (e.g. HL7) libraries and message processing components. Components in this namespace are independent of Apache Camel and have no dependencies to platform-camel.
osgi Namespace for IPF OSGi infrastructure and configuration bundles.
platform-camel Namespace for extensions to the Apache Camel routing and mediation engine. These extensions together with Apache Camel are referred to as the Open eHealth Integration Platform (IPF) from a technical viewpoint.
tutorials Namespace for tutorials demonstrating the features of the Open eHealth IPF (not shown in any of the figures).
archetypes Namespace for IPF project archetypes (not shown in any of the figures).
Component Description
commons-core A library that defines the common message processing API to be implemented by components contained in the modules folder. platform-camel provides special integration points for components implementing this common API. The API was defined for implementing message processing components independent of a certain integration infrastructure like Apache Camel. This increases their re-usability and allows applications to implement light-weight message processing functionality without implementing an integration platform or enterprise service bus.
commons-flow A library implementing the flow management services. It is used to monitor, query, audit and replay message flows. The management interfaces are based on JMX. An integration into Apache Camel is provided by platform-camel-flow.
commons-ihe-atna A library used by other IPF IHE components to implement IHE ATNA.
commons-ihe-xds-* Libraries used to implements IHE XDS.a and XDS.b actor interfaces. The libraries are Camel-independent
commons-map A code-mapping library.
commons-lbs A library providing storage for large binaries and javax.activation.DataSource implementations to represent stored binaries.
commons-event A library containing the Camel independent functionality of the Event infrastructure.
commons-test A library providing support for HTTP-based integration and performance tests.
commons-xml An XML library with focus on Schematron validation.
modules-cda A library for creating, parsing, navigating and rendering CDA documents. Also includes support for several CDA profiles.
modules-hl7 A library that implements extensions to the HAPI library.
modules-hl7dsl A library that implements a DSL for manipulating HAPI messages.
osgi-commons See IPF bundle overview.
osgi-extender-basic See IPF bundle overview.
osgi-extender-spring See IPF bundle overview.
osgi-config-log See IPF bundle overview.
osgi-config-jms See IPF bundle overview.
osgi-config-flow-repository See IPF bundle overview.
osgi-config-flow-manager See IPF bundle overview.
platform-camel-core A component that is required by most other platform-camel component. It provides:
platform-camel-flow A component that implements a flow management DSL for integrating commons-flow into Camel routes. The DSL was built with the DSL extension mechanism.
platform-camel-event A component offering the DSL extensions of the event infrastructure
platform-camel-hl7 A component that implements an HL7 message processing DSL for integrating modules-hl7 and modules-hl7dsl into Camel routes. The DSL was built with the DSL extension mechanism.
platform-camel-lbs-core A component offering the DSL extensions of the large binary support mechanism as well as the interface to adapt various endpoint technologies to the LBS.
platform-camel-lbs-cxf A component with adaptor implementations for using CXF endpoints with the large binary support.
platform-camel-lbs-http A component with adaptor implementations for using HTTP/Jetty endpoints with the large binary support.
platform-camel-lbs-mina A component with adaptor implementations for using MINA/HL7 endpoints with the large binary support.
platform-camel-ihe-mllp-* Provides MLLP-related functionality used by IPF PIX and PDQ components.
platform-camel-ihe-pix-* Components to implement actor interfaces of IHE PIX transactions. See IHE support.
platform-camel-ihe-pdq-* Components to implement actor interfaces of IHE PDQ transactions. See IHE support.
platform-camel-ihe-xds-* Components to implement actor interfaces of IHE XDS.a and XDS.b transactions. See IHE support.
platform-camel-test A component for measuring the performance characteristics of IPF applications. It provides DSL extensions used to measure processing time of messages (for route parts and whole routes) and the message throughput. Performance measurement reports can be created as well.
tutorials-basic A Hello World-level tutorial for getting started with platform-camel
tutorials-flow A tutorial demonstrating IPF flow management features.
tutorials-hl7 A tutorial demonstrating HL7 message processing features.
tutorials-lbs A tutorial demonstrating the use of the LBS with CXF and HTTP endpoints.
tutorials-osgi-* A tutorial demonstrating how to write IPF OSGi applications.
tutorials-ref A complete reference application that may be used as blueprint for production-ready IPF applications. The reference application is a comprehensive demonstration of IPF features.
tutorials-xds A tutorial that demonstrates how to use IPF XDS components.
ipf-archetype-* Archetypes for creating IPF integration projects with a ready-to-use Maven 2 and Eclipse configuration plus an example route and unit tests.

Scripting layer

IPF message processing routes are written with a domain-specific language (DSL) which is based on the Apache Camel DSL. Any DSL feature from Apache Camel is also available within IPF. One important design goal of IPF was to provide healthcare-specific message processing capabilities as extensions to the Camel DSL. For example, implementing an HL7 message validator should be as easy as implementing a XSD validator. To make DSL extensions possible IPF provides a DSL extension mechanism which is based on Groovy meta-programming. A meta-programming based approach was chosen because Camel currently doesn't provide an interface for extending its DSL. Consequently, for using the IPF DSL (i.e. the Camel DSL plus IPF DSL extensions) message processing routes must be written in the Groovy programming language. This doesn't mean that all parts of your program must be written in Groovy e.g. custom message transformers can still be implemented in Java (or any other JVM language). It is only important that route definitions (i.e the route builder) are written in Groovy.

All predefined DSL extensions provided by IPF have been implemented using this extensions mechansim. One important aspect of the extension mechanism is that it can also be used by IPF application to implement application-specific DSL extensions. There is a separate DSL extensions guide that explains how to. This makes application-specific message processing routes more readable and allows other projects to reuse these extensions. DSL extensions may also be based on other DSL extensions i.e. composition of DSL extensions is supported as well.

The DSL extension mechanism including all DSL extensions contributed by individual IPF components are referred to as the IPF scripting layer. The feature set of the IPF DSL therefore depends on which IPF components have been deployed. In other words, the DSL extension mechanism supports modularization of DSL extensions. An overview of all predefined IPF DSL extensions is given in the IPF extensions index. The following subsection exaplains the DSL extension mechanism in more detail.

DSL extension mechanism

DSL extensions guide and tutorials

The example presented here is explained in much more detail in the DSL extensions guide. Furthermore, all tutorials make use of the DSL extension mechanism and the predefined DSL extensions.

The easiest way to learn the DSL extension mechanism is to start with a simple example. Let's say we want to introduce a new translate DSL element that translates a message from one language into another. To keep the examle simple we assume that the translation magic is done inside a single Translator processor.

Translator.java
package example;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;

public class Translator implements Processor {

    public void process(Exchange exchange) throws Exception {
        // do language translation here ...
    }

}

Without using DSL extensions such a processor can be included into route definitions like in the following example.

MyRouteBuilder.groovy
package example

import org.apache.camel.spring.SpringRouteBuilder

class MyRouteBuilder extends SpringRouteBuilder {

    void configure() {
        from('direct:input')
        .process(new Translator())
        .to('direct:output')

    }

}

The goal however is to introduce a translate DSL extension that makes use of this processor. The result should look like.

MyRouteBuilder.groovy
package example

import org.apache.camel.spring.SpringRouteBuilder

class MyRouteBuilder extends SpringRouteBuilder {

    void configure() {
        from('direct:input')
        .translate()
        .to('direct:output')

    }

}

To be able to use the translate() method in our route definitions we need to add that method into the corresponding DSL model class which is org.apache.camel.model.ProcessorDefinition in our example. Adding the method is done via Groovy meta-programming inside an extensions block.

MyExtension.groovy
package example

import org.apache.camel.model.ProcessorDefinition

class MyExtension {

    static extensions = {

        ProcessorDefinition.metaClass.translate = {->
            delegate.process(new Translator())
        }

    }

}

You can add an extensions block to any class you like. To make the example work, the DSL extension, the route builder and the infrastructure components of the DSL extension mechanism must be wired together within a Spring application context.

context.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/spring 
http://camel.apache.org/schema/spring/camel-spring.xsd">

    <camel:camelContext id="camelContext">
        <camel:routeBuilder ref="routeBuilder"/>        
    </camel:camelContext>
    
    <bean id="routeBuilder" depends-on="routeModelExtender"
        class="example.MyRouteBuilder">
    </bean>
    
    <bean id="exampleModelExtension"
        class="example.MyExtension">
    </bean>
    
    <bean id="coreModelExtension"
        class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension">
    </bean>
    
    <bean id="routeModelExtender"
        class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
        <property name="routeModelExtensions">
            <list>
                <ref bean="coreModelExtension" />
                <ref bean="exampleModelExtension" />
            </list>
        </property>
    </bean>

</beans>

The above example also shows how to include the predefined DSL extensions provided by platform-camel-core. Please note that this is not required to make our example work. It's only an example how to combine predefined DSL extensions with custom DSL extensions.

DSL extensions and OSGi

In an OSGi environment DSL extensions are automatically detected by IPF-specific extender bundles, so there is no need to add them to a Spring application context.

Limitations

There are some limitations you should be aware of when using the IPF extension mechanism. DSL extensions defined in Groovy are not available for Camel's Java DSL or for the Spring-based XML configuration. Also, DSL extensions currently do not show up for code completion in IDEs such as the Groovy plugins for Eclipse or IntelliJ IDEA. However, we are currently working on a solution for the code-completion problem.

Predefined DSL extensions

Predefined DSL extensions are provided by the following IPF components.

Component Extension class Documentation
platform-camel-core org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension Core features DSL
platform-camel-flow org.openehealth.ipf.platform.camel.flow.extend.FlowModelExtension Flow management DSL
platform-camel-hl7 org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension HL7 processing DSL
platform-camel-lbs org.openehealth.ipf.platform.camel.lbs.core.extend.LbsModelExtension LBS processing DSL
platform-camel-event org.openehealth.ipf.platform.camel.event.extend.EventModelExtension Event processing DSL
platform-camel-ihe-xds org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension IHE support
platform-camel-test org.openehealth.ipf.platform.camel.test.performance.extend.PerformanceModelExtension Performance measurement

A description of these components is given on the IPF architecture page. For a detailed description of the extensions follow the links in the Documentation column. The extension classes can be combined as shown in the configuration example above.

DSL extensions index

A summary of all predefined DSL extensions is given in the DSL extensions index.

Core features

The content of this page describes the features provided by the platform-camel-core component. These are the the module adapters and the core DSL extensions. For an overview how platform-camel-core fits into its architectural context refer to the IPF architecture page.

Module adapters

Module adapters integrate components of the modules (and partly the commons) namespace into Camel routes. Components defined in these namespaces implement interfaces defined in the org.openehealth.ipf.commons.core.modules.api package which is part of the commons-core component. These interfaces represent message processors like Validator, Aggregator or Transmogrifer (a transformer), to mention a few. The intention of these interfaces was to define message processors that are independent of integration infrastructures like Apache Camel in order to increase their reusability in other contexts. The integration of these message processors into IPF routes is done via generic module adapters. They translate from Camel-specific interfaces to org.openehealth.ipf.commons.core.modules.api.* interfaces and are provided by the platform-camel-core component. The following UML diagram gives an overview (some of the relevant interfaces and adapters have been omitted). For details how to use module adapters in IPF routes refer to the DSL extensions for IPF module adapters section.

Module adapter alternatives

An alternative to module adapters is Camel's bean integration mechanism.

DSL extensions

DSL extension for existing Camel features

Closure support

Several Camel DSL elements have been extended to accept closures as arguments. For example, the following processor closure reverses the string contained in the input message body.

'processor' closure
from('direct:input')
    .process {exchange ->
        exchange.in.body = exchange.in.body.reverse()
    }
    .to('mock:output')

The closure parameter is the org.apache.camel.Exchange to be processed.


A filter closure is a closure that evaluates an org.apache.camel.Exchange and returns either true or false i.e. a filter closure is a predicate:

'filter' closure
from('direct:input')
    .filter {exchange ->
        exchange.in.body == 'blah'
    }
    .to('mock:output')

Here, the filter closure returns true if the in-message body equals 'blah'. If true is returned the filter forwards the message to the next processor, otherwise, the message is filtered out.


Closures can also be used for setting message headers. In the following example the foo-header is assigned the in-message's body.

'setHeader' closure
from('direct:input')
    .setHeader('foo') {exchange ->
        exchange.in.body
    }
    .to('mock:output')

The header name is given as first setHeader argument. The header value is the return value of the closure. You can also use closures with setOutHeader and setFaultHeader.


To set an exchange property you can use a setProperty-closure.

'setProperty' closure
from('direct:input')
    .setProperty('foo') {exchange ->
        exchange.in.body
    }
    .to('mock:output')


You can also set the in-message body with a closure. In the following example the in-message body is set to the value of the in-message foo-header.

'setBody' closure
from('direct:input')
    .setBody {exchange ->
        exchange.in.headers.foo
    }
    .to('mock:output')


The transform DSL element also supports expression closures. For example, to reverse a string contained in the in-message body and write the result to the out-message body use:

'transform' closure
from('direct:input')
    .transform {exchange ->
        exchange.in.body.reverse()
    }


Closure support was also added for content-based routing:

'when' closure
from('direct:input1')
    .choice()
    .when { it.in.body == 'a'}
        .to('mock:output1')
    .when { it.in.body == 'b'}
        .to('mock:output2')

when-closures implement routing decisions and return either true or false. In our example: If the in-message body equals a the message is forwarded to endpoint mock:endpoint1. If the in-message body equals b the message is forwarded to endpoint mock:endpoint2.


The onWhen DSL element allows for finer-grained exception handling. With IPF you can implement onWhen predicates with closures. The following example tries to match the in-message body with a regular expression.

'onWhen' closure
onException(Exception.class)
  .onWhen {it.in.body ==~ /u.w/}
  .to('direct:error')

Bean lookup


The process DSL element can also be used in combination with a bean name. The following route definition references a bean named sampleProcessor from the application context. The bean must implement the org.apache.camel.Processor interface. This is equivalent to Camel's processRef(String).

Processor bean
from('direct:input1')
    .process('sampleProcessor')
    .to('mock:output')

Error handler


Another convenience DSL extension provided by IPF is the unhandled DSL element. It replaces the verbose errorHandler(noErrorHandler()) statement in route definitions i.e. it drops the error handler from a route.

'unhandled' extension
from('direct:input')
    .unhandled()
    // further processing here ...
    .to('mock:output')

The unhandled extension currently cannot be used globally for all routes i.e. a builder.unhandled() call is not supported yet.

Interceptor

Camel's 1.x support for the intercept DSL element has been removed in Camel 2.0. For backwards compatibility-reasons, IPF re-introduces intercept as DSL extension. Valid argument types are org.apache.camel.DelegateProcessor and groovy.lang.Closure.

'intercept' extension using a delegate processor
from('direct:input1')
    .intercept(new MyDelegateProcessor())
    .to('mock:output')

'intercept' extension using a closure
from('direct:input1')
    .intercept {exchange, next ->
        // do some pre-processing work
        // ...

        // proceed with the next processor
        next.proceed(exchange)

        // do some post-processing work ...
        // ...
    }
    .to('mock:output')

The intercept closure is passed a second argument (called next in our example) on which the proceed() method must be called. This method continues processing with the next processor and returns after down-stream processing has been done.

'intercept' extension using a bean
from('direct:input1')
    .intercept('InterceptorBean')
    .to('mock:output')

The bean must implement the interface org.openehealth.ipf.platform.camel.core.process.Interceptor, e.g.:

'Implementation of an interceptor'
import org.apache.camel.Exchange;
import org.apache.camel.Processor;

import org.openehealth.ipf.platform.camel.core.process.Interceptor;

public class MyInterceptor implements Interceptor {
    @Override
    public void process(Exchange exchange, Processor next) throws Exception {
        exchange.getIn().setBody("before");        // Do something before
        next.process(exchange);                    // Call the intercepted processor
        exchange.getOut().setBody("after");        // Do something afterwards
    }
}

DSL extensions for ExpressionClause

Exception objects and messages

The model class org.apache.camel.builder.ExpressionClause has been extended with expressions to access the exception object or the exception message of an exchange. The corresponding DSL extensions are exceptionMessage and exceptionObject. For example,

'exceptionMessage' extension
from('direct:input')
    .onException(ValidationException.class).setBody().exceptionMessage().to('mock:error').end()
    // ... ValidationException thrown here ...
    .to('mock:output')

sets the detail message of the ValidationException (obtained via ValidationException.getMessage) to the in-message body of the exchange. This extensions is particularly useful in exception routes because there you don't have access to the exception object via Exchange.getException(). In the next example we set the exception object on a custom header.

'exceptionObject' extension
from('direct:input')
   .onException(ValidationException.class).setHeader('foo').exceptionObject().to('mock:error').end()
    // ... ValidationException thrown here ...
    .to('mock:output')

Here, the in-message that arrives at mock:error has a foo header with the ValidationException object.

DSL extensions for custom IPF processors

IPF provides message processors that have been developed on top of Apache Camel. Initially, they were included into route definitions via the default extension points provided by Camel (e.g. the org.apache.camel.Processor or org.apache.camel.Predicate interfaces). With the IPF scripting layer, they are now available as first-class DSL elements. This section presents those DSL extensions that are independent of the IPF module adapters. The next section is about module-adapter-related DSL extensions.

Content enrichment

Content enricher contributed to Camel

The IPF content enricher has been contributed to Camel and is available there since version 2.0-m1. We will drop the content enricher from the IPF code base as soon as we upgraded to Camel 2.0. However, we will continue to support merge closures (see below). The contributed content enricher is also documented in the Camel Wiki.

The content enricher creates an additional message exchange from the original exchange for communicating with a so-called resource endpoint. This endpoint is used to obtain additional data to enrich the original exchange. The response from the resource endpoint is merged into the original message exchange using a configurable merge logic. In the following example the merge logic is defined by a closure:

'enrich' extension using a merge closure
from('direct:input')
    .enrich('direct:resource') {originalExchange, resourceExchange ->
        originalExchange.in.body += ':' + resourceExchange.out.body
        // return value is optional if originalExchange
        // shall be returned as merge result, otherwise
        // it must be returned explicitly.
        originalExchange
    }
    .to('mock:output')

In this example, the enricher obtains additional data from the direct:resource endpoint and passes the originalExchange and resourceExchange to a closure that merges data from the resourceExchange into originalExchange. The message exchange containing the merge result is returned by the closure. Returning an exchange is optional if the originalExchange object contains the merge result. Instead of providing merge logic via a closure you may also provide an org.apache.camel.processor.aggregate.AggregationStrategy instance.

'enrich' extension using an AggregationStrategy object
AggregationStrategy strategy = new MyCleverAggregationStrategy()

from('direct:input')
    .enrich('direct:resource', strategy)
    .to('mock:output')

Validation process

The validation DSL extension implements a simple validation process that delegates the actual message validation to a validator which can be an endpoint or an object that implements validation logic. If validation succeeds the message is forwarded to the next processor defined in the route, otherwise, the message is dropped. In both cases the response generated by the validator is returned to the sender of the original message exchange.


In the following example the validation process delegates message validation to the direct:validator endpoint. If validation succeeds the message is forwarded as in-only exchange to mock:output otherwise it is dropped. The response returned from direct:validator is returned to the direct:input endpoint.

'validation' extension using an endpoint
from('direct:input')
    .validation('direct:validator')
    .to('mock:output')

The validation process interprets a message exchange as failed if any of the following conditions is true:

  • an exception was thrown
  • the message exchange contains an exception
  • the message exchange contains a fault message (i.e. an out message marked as fault)


Instead of providing an endpoint you may also provide validator logic using a closure.

'validation' extension using a closure
from('direct:input1')
    // generate a 'success' validation response
    .validation {exchange -> exchange.out.body = 'success'}
    .to('mock:output')

from('direct:input2')
    // generate a 'failure' validation fault
    .validation {exchange -> exchange.out.body = 'failed'; exchange.out.fault = true}
    .to('mock:output')

from('direct:input3')
    // throw a validation exception
    .validation {throw new ValidationException('input sucks in any case')}
    .to('mock:output')


You may also provide an org.apache.camel.Processor instance for validation

'validation' extension using a processor
Processor validator = new MyFamousValidator()

from('direct:input')
    .validation(validator)
    .to('mock:output')

Splitter

The Splitter is used to split a message into multiple messages. You specify a split rule that defines the way the splitting is performed. Each message generated by the Splitter is send to the next processor defined in the route. The results of this processing are aggregated into the original message using a configurable aggregation strategy.

IPF and Camel splitters

The splitter implemented by IPF differs from Camel's splitter. To choose between these two implementation use .split() for Camel's splitter and ipf().split() for IPF's splitter.


The following shows a String-based example of the Splitter that splits a comma-separated string coming from direct:input and passes the parts on to the mock:output endpoint. If you send the body "hello,world,!" to direct:input, the mock:output will receive three messages with "hello", "world" and "!" in the body.

'split' extension using a closure
from('direct:input')
    .ipf().split { Exchange exchange -> exchange.in.body.split(',') }
    .to('mock:output')


The split rule can also be defined via org.apache.camel.Expression:

'split' extension using an expression
Expression splitExpression = new MyCommaSplittingExpression()

from('direct:input')
    .split(splitExpression)
    .to('mock:output')

No matter how the split rule is defined, the aggregation of all results is by default performed using the UseLatestAggregationStrategy. This strategy simply uses the result of the last message generated by the split. In the above example, "!" is returned to the sender, because it is the last message that was split off.

You can specify a different strategy using aggregationStrategy after the split processor. The following example extends the previous one generating an output containing the parts reassembled and separated by a ':'. Therefore, the sender will get the result "hello:world:!".

'split' extension using an explicit aggregation strategy

from('direct:input')
    .ipf().split { Exchange exchange -> exchange.in.body.split(',') }
    .aggregationStrategy { oldExchange, newExchange ->
          String oldContent = oldExchange.in.body
          String newContent = newExchange.in.body
          Exchange aggregate = oldExchange.copy()
          aggregate.in.body = oldContent + ":" + newContent
          aggregate
    }
    .to('mock:output')

The aggregation strategy is called only if the split resulted in multiple messages. If no messages were generated, the result is the original message. If only one message was split off, this message is used as the result. In the above example, the aggregation strategy is called twice with the following messages as parameters:

First call: oldExchange = "hello", newExchange = "world" -> returns "hello:world"
Second call: oldExchange = "hello:world", newExchange = "!" -> returns "hello:world:!"

DSL extensions for IPF module adapters

How this section is organized

This section is organized in the following way.

  • Detailed description of the transmogrify extension. Many concepts described here also apply to other module adapter extensions.
  • Overview of the remaining module adapter extensions. This overview only covers topics that haven't been discussed for the transmogrify extension.
  • Comprehensive summary of all module adapter extensions. A complete reference for module adapter extensions in tabular form (without detailed examples).

Transmogrifier

The easiest way to describe the DSL extensions for IPF module adapters is to start with an example. Let's use the org.openehealth.ipf.commons.core.modules.api.Transmogrifier interface for that purpose. Inspired by Calvin and Hobbes, a transmogrifier converts anything into whatever you like. Transmogrification is accompanied by a loud zap:

Transmogrifier.java
public interface Transmogrifier<S, T> {

    T zap(S object, Object... params);

}

Implementations of Transmogrifier are often used for message transformation. Transformation input is given by the object parameter and optionally some additional params. The transformation result is the return value of the zap method. To include a Transmogrifier instance into a Camel route we use the transmogrify DSL extension:

'transmogrify' extension using a transmogrifier object
org.openehealth.ipf.commons.core.modules.api.Transmogrifier transmogrifier = new MyTransmogrifier()
from('direct:input') 
    .transmogrify(transmogrifier)
    .to('mock:output')

Behind the scenes the transmogrify element creates an org.openehealth.ipf.platform.camel.core.adapter.TransmogrifierAdapter as described in the module adapters section. This adapter adapts the org.openehealth.ipf.commons.core.modules.api.Transmogrifier interface to an org.apache.camel.Processor interface. The adapter (processor) is included into the Camel route at the position where the transmogrify extension is used. During message processing the adapter accepts an org.apache.camel.Exchange, extracts the input from that exchange, delegates message processing to the adapted transmogrifier instance and populates the exchange with the transformation result.

Inclusion options

There are three different ways of including a transmogrifier into a Camel route.

  1. Pass a transmogrifier object as argument to the transmogrify() method. This has already been shown in the example route above.
  2. Pass the name of a transmogrifier bean as argument to the transmogrify method (see below). A bean with that name must exist in the Spring application context.
  3. Define a transmogrifier logic inline using a closure (see below). This is comparable to implement an anonymous Transmogrifier class.

'transmogrify' extension using a transmogrifier bean
from('direct:input') 
    .transmogrify('myTransmogrifierBean')
    .to('mock:output')

'transmogrify' extension using a closure
from('direct:input') 
    .transmogrify { body, headers ->
        def result = ... // create result from input body and headers
        return result    // return the transformation result
    }
    .to('mock:output')
Inclusion pattern

This is a pattern that also applies to all other DSL extensions for IPF module adapters: validate, parse, render, predicate and aggregationStrategy. The adapted object can either be included into a Camel route directly as object, indirectly via a Spring bean name or defined (inline) with a closure. Closures are not supported for parse and render.

Transmogrifier input

Default arguments to the Transmogrifier.zap(S object, Object... params) method are:

  • The in-message body for the object parameter.
  • The in-message headers for the params parameter.

A transmogrify closure may define a one or two parameters.

  • The first parameters corresponds to the object parameter.
  • The second parameter corresponds to the params parameter.

of the zap method.

  • The default argument to the first closure parameter is the in-message body.
  • The default argument to the second closure parameter are the in-message headers (a java.util.Map).

Input to the zap method as well as the transmogrify closure can be customized via the

  • input
  • params and
  • staticParams

DSL extensions. input and params either accept an org.apache.camel.Expression as argument or an expression closure. In both cases an org.apache.camel.Exchange is evaluated. The evaluation result will be used as transmogrifier input. The following snippet causes the transmogrifier's zap method to be called with the in-message's foo-header as first argument and the in-messages bar-header as the second argument

'transmogrify' parameterization with input and params
from('direct:input') 
    .transmogrify('myTransmogrifierBean')
    .input { exchange -> exchange.in.headers.foo }
    .params { exchange -> exchange.in.headers.bar }
    .to('mock:output')

The same rules apply for the transmogrify closure parameters.

'transmogrify' parameterization with input and params
from('direct:input') 
    .transmogrify { fooHeader, barHeader ->
        // ...
    }
    .input { it.in.headers.foo }
    .params { it.in.headers.bar }
    .to('mock:output')

The params DSL extension also supports predefined expressions. These are accessible by calling params with no arguments. The following predefined expressions are currently supported as part of the DSL.

Predefined expressions for 'params'
    ...
    .params().headers()           // in-message headers (default for transmogrifiers)
    ...
    .params().header('foo')       // in-message foo-header
    ...
    .params().builder()           // a Groovy XML builder
    ...
    .params().headersAndBuilder() // in-message headers and a Groovy XML builder (params array of length 2)
    ...

These predefined expressions are implemented by the org.openehealth.ipf.platform.camel.core.model.ParamsDefinition model class. The builder and headersAndBuilder extensions are described in section DSL extensions for Groovy XML processing.

The staticParams extension can be used to pass constant values to the transmogrifier or transmogrifier closure. This extension method defines a variable argument parameter. For example to pass a String array with elements 'a', 'b' and 'c' via the params parameter or via the second closure parameter you could use

'transmogrify' parameterization with staticParams
from('direct:input') 
    .transmogrify('myTransmogrifierBean')
    .staticParams('a', 'b', 'c')
    .to('mock:output')
'transmogrify' parameterization with staticParams
from('direct:input') 
    .transmogrify { body, stringArray ->
        // ...
    }
    .staticParams('a', 'b', 'c')
    .to('mock:output')
Input customization for other adapter extensions
Transmogrifier output

The return value of the Transmogrifier.zap(S object, Object... params) method or the return value of the transmogrify closure is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain).

Transmogrifier implementations

IPF provides two Transmogrifier implementations out of the box:

  • org.openehealth.ipf.commons.xml.XsltTransmogrifier for transforming XML documents
  • org.openehealth.ipf.commons.xml.SchematronTransmogrifier for creating Schematron validation reports from XML documents

These implementations are "by-products" for Schematron validation, but you can use them independently as well. Compared to Camel's xslt endpoint (http://camel.apache.org/xslt.html), the IPF counterpart

  • can use variable stylesheets
  • caches XSLT templates for better performance
  • accepts explicit XSLT parameters (not just as Camel message header)

The input is automatically converted into a StreamSource. By default, all Camel headers are added as parameters which are available in the stylesheet unless you define parameters by either using params(...) or staticParams(...).

Example:

IPF transmogrifiers
from('direct:input1') 
    .transmogrify().xslt().staticParams('path/to/stylesheet') // static stylesheet
    .to('mock:output')

from('direct:input1') 
    .transmogrify().xslt().staticParams('path/to/stylesheet', parameterMap) // static stylesheet with parameters
    .to('mock:output')

from('direct:input3')
    .setHeader('stylesheet', constant('path/to/stylesheet')) 
    .transmogrify().xslt().params().header('stylesheet') // dynamic stylesheet
    .to('mock:output')

// In most cases you will need the SchematronValidator, which scans the Schematron report
// for failed assertions. Use only if you require custom processing of the report in the
// route.

from('direct:input3') 
    .transmogrify().schematron().staticParams('path/to/rules', options ) // static rules
    .to('mock:output')

from('direct:input4') 
    .setHeader('rules', constant('path/to/rules')) 
    .transmogrify('schematron').params().header('rules') // dynamic rules
    .to('mock:output')

Please refer to Schematron validation for a description of the available Schematron options.

By default, XSLT transformations return a javax.xml.transform.Result object, which is, however, not very useful for further processing. IPF's XSLT-related transmogrifiers therefore return a String by default. You can influence the returned type using a Class parameter to the xslt()/schematron() extensions or with a subsequent call to convertBodyTo(Class):

IPF transmogrifiers with output type
from('direct:input1') 
    .transmogrify().xslt(InputStream.class).staticParams('path/to/stylesheet') 
    .to('mock:output')

from('direct:input2') 
    .transmogrify().xslt().staticParams('path/to/stylesheet') 
    .convertBodyTo(InputStream.class)
    .to('mock:output')

Validator

The modules API defines an org.openehealth.ipf.commons.core.modules.api.Validator interface for message validation.

Validator.java
public interface Validator<S, P> {

    void validate(final S message, final P profile);

}

It defines a single validate method that validates a message against a profile. If validation fails an org.openehealth.ipf.commons.core.modules.api.ValidationException is thrown. The validator is included into Camel routes via the validate DSL extension. The validate extension accepts either a validator object, a validator bean name or a validator closure as argument. If a closure is used, a failed validation is either indicated by throwing an org.openehealth.ipf.commons.core.modules.api.ValidationException or by returning false. If false is returned IPF internally generates a ValidationException. Here are some examples.

'validate' extension
// route 1
from('direct:input1') 
     .validate {body -> body == 'blah'}  
     .to('mock:output')

// route 2
from('direct:input2') 
     .validate {throw new ValidationException('always fail')}  
     .to('mock:output')

// route 3
from('direct:input3') 
     .validate {body, profile -> 
         body == profile
     }
     .staticProfile('blah')
     .to('mock:output')

// route 4
from('direct:input4') 
     .validate {fooHeader, profile -> 
         fooHeader == profile
     }
     .input {it.in.headers.foo}
     .staticProfile('abcd')
     .to('mock:output')

// route 5
from('direct:input5') 
     .validate(...)
     .input(...)
     .profile {exchange ->        
         exchange.in.headers.customProfile
     }                         
     .to('mock:output')

// you may also use validator objects
     ...
     .validate(new MyCustomValidator())
     ...

// you may also use validator beans
     ...
     .validate('myValidatorBean')
     ...
  • In route 1 validation will fail if the in-message body doesn't equal 'blah'. In this case false is returned which causes IPF to throw a ValidationException.
  • In route 2 validation will fail because a ValidationException is thrown directly (regardless of the message content).
  • In route 3 we define a closure with a second parameter for passing a validation profile. By default it is null but it can be customized via the staticProfile DSL extension *). As in route 1 the validation will fail if the in-message body doesn't equal 'blah'.
  • In route 4 we see how input is used to pass the in-message's foo-header as first argument to the validation closure. If the foo-header doesn't equal 'abcd' validation will fail.
  • In route 5 a validation profile is obtained from the in-messages's customProfile header using the profile() DSL extension and a closure. Instead of the closure one can also use an org.apache.camel.Expression instance.

*) In this example we could have hard-coded this profile directly inside the closure as well. Using the profile extension makes more sense when using validator objects or beans like in validate(myValidator) or validate('myValidatorBean').


As of version 1.7, IPF provides a Validator implementation that validates an XML Source against an W3C XML Schema.

'xsd' extension

from('direct:input1') 
     .validate().xsd().staticProfile('schema location')
     .to('mock:output')

The schema location value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource.


As of version 1.7, IPF also provides a Validator implementation that validates an XML Source against a set of Schematron rules.

'schematron' extension

import org.openehealth.ipf.commons.xml.SchematronProfile;
...
from('direct:input1') 
     .validate().schematron().staticProfile(new SchematronProfile('rules location', options))
     .to('mock:output')

Note that you have to provide an instance of SchematronProfile, not just the plain Schematron rules location. The rules location value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource.
The options parameter is optional. If present, it must be of type Map<String, Object> Its purpose is to configure Schematron's validation process. Please refer to the Schematron website http://www.schematron.com for more details.

key description values default
phase Select the phase for validation. Schematron allows for staged validation by assigning phases to validation rules. NMTOKEN | #ALL #ALL
allow-foreign Pass non-Schematron elements and rich markup to the generated stylesheet 'true' | 'false' 'false'
diagnose Add the diagnostics to the assertion test in reports 'true' | 'false' 'true'
property Experimental: Add properties to the assertion test in reports 'true' | 'false' 'true'
generate-paths Generate the @location attribute with XPaths 'true' | 'false' 'true'
sch.exslt.imports semi-colon delimited string of filenames for some EXSLT implementations string ''
optimize Use only when the schema has no attributes as the context nodes 'visit-no-attributes' ''
generate-fired-rule Generate fired-rule elements. Significantly increases report size 'true' | 'false' 'true'

Parser

The modules API org.openehealth.ipf.commons.core.modules.api.Parser interface declares methods for parsing an external representation of information into an internal model.

Parser.java
public interface Parser<S> {

    S parse(String message, Object... params);
    S parse(InputStream message, Object... params) throws IOException;
    S parse(Source source, Object... params) throws IOException;
    S parse(Reader reader, Object... params) throws IOException;

}

The external representation can be obtained from a java.io.InputStream, a java.io.Reader, a javax.xml.transform.Source or directly from a java.lang.String. These options are represented by the four parse methods. A parser is included into Camel routes via the parse DSL extension. IPF selects the appropriate method depending on the type of input data. The return value is the parse result and is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain). Here are some examples.

'parse' extension
// route 1
from('direct:input1')
    .parse(new MyParser())
    .to('mock:output')

// route 2
from('direct:input2')
    .parse('myParserBean')
    .input { it.in.headers.foo }
    .params { it.in.headers.bar }
    .to('mock:output')
  • In route 1 we directly include a MyParser object into the Camel route. Here, the parser input data are taken from the in-message body, the parser params are null.
  • In route 2 we include a Spring bean with name 'myParserBean' into the Camel route. Here, the parser's input data are taken from the in-message's foo-header, the parser params from the bar-header.

Closures for parse are currently not suppported.

Unmarshalling via Parser

IPF also provides an org.apache.camel.spi.DataFormat implementation that delegates unmarshal work to a parser.

'parse' extension for unmarshalling
from("direct:input1")
    .unmarshal().parse(new MyParser())
    ...

from("direct:input2")
    .unmarshal().parse('myParserBean')
    ...

However, using parse for unmarshalling currently doesn't allow input customization via input, params or staticParams.

Renderer

The modules API org.openehealth.ipf.commons.core.modules.api.Renderer interface declares methods for creating an external representation of an internal model.

Renderer.java
public interface Renderer<T> {

    Result render(final T model, Result result, final Object... params) throws IOException;
    OutputStream render(final T model, OutputStream result, final Object... params) throws IOException;
    Writer render(final T model, Writer result, final Object... params) throws IOException;
    String render(final T model, final Object... params);

}

Currently, only the last method i.e. the one that returns a java.lang.String is used by IPF. A renderer is included into Camel routes via the render DSL extension. IPF selects the appropriate method depending on the type of input data. The return value is the rendering result and is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain). Here are some examples.

'render' extension
// route 1
from('direct:input1')
    .render(new MyRenderer())
    .to('mock:output')

// route 2
from('direct:input2')
    .render('myRendererBean')
    .input { it.in.body[0] }
    .params { it.in.headers.bar }
    .to('mock:output')
  • In route 1 we directly include a MyRenderer object into the Camel route. Here, the renderer input data are taken from the in-message body, the renderer params are null.
  • In route 2 we include a Spring bean with name 'myRendererBean' into the Camel route. Here, the renderer's input data are taken from the first element of a list that is contained in the in-message's body. The renderer params are taken from the in-messages bar-header.

Closures for render are currently not suppported.

Marshalling via Renderer

IPF also provides an org.apache.camel.spi.DataFormat implementation that delegates marshal work to a renderer.

'render' extension for marshalling
from("direct:input1")
    .marshal().render(new MyRenderer())
    ...

from("direct:input2")
    .unmarshal().render('myRendererBean')
    ...

However, using render for marshalling currently doesn't allow input customization via input, params or staticParams.

Predicate

The org.openehealth.ipf.commons.core.modules.api.Predicate interface declares a matches method for evaluating a binary predicate on a source object.

'Predicate'
public interface Predicate<T> {

    boolean matches(T source, Object... params);

}


The predicate DSL extension can be used to include these predicates into Camel routes. It is implemented with the org.openehealth.ipf.platform.camel.core.adapter.PredicateAdapter that translates between org.openehealth.ipf.commons.core.modules.api.Predicate and org.apache.camel.Predicate. The predicate extension supports a predicate object, a bean name or a closure as argument. The created Camel predicate can then be used with e.g. filter or other DSL elements that expect an org.apache.camel.Predicate. The predicate extension is provided by an IPF extension to org.apache.camel.spring.SpringRouteBuilder. Here are some examples:

'predicate' extension
import org.apache.camel.spring.SpringRouteBuilder
import org.openehealth.ipf.commons.core.modules.api.Predicate
...

class MyRouteBuilder extends SpringRouteBuilder {
    void configure() {
        Predicate myPredicate = new MyPredicate()

        def predicate1 = predicate(myPredicate)
        def predicate2 = predicate('myPredicateBean') 
        def predicate3 = predicate { body -> body == 'test'}

        from('direct:input1').filter(predicate1).to('mock:output')
        from('direct:input2').filter(predicate2).to('mock:output')
        from('direct:input3').filter(predicate3).to('mock:output')
...

Input to the org.openehealth.ipf.commons.core.modules.api.Predicate.matches() method can be customized via the input, params or staticParams DSL extensions.

'predicate' extension with input customization
def predicate4 = 
    predicate { fooHeader, barHeader -> ... }
        .input { it.in.headers.foo }
        .params { it.in.headers.bar }

Aggregator

The org.openehealth.ipf.commons.core.modules.api.Aggregator interface is a transmogrifier that combines/aggregates a collection of input object into a result object. The result object is the return value of the zap method.

Aggregator.java
public interface Transmogrifier<S, T> {
    T zap(S object, Object... params);
}

public interface Aggregator<S, T> extends Transmogrifier<Collection<S>, T>

{anchor:aggregationStrategy}}
The aggregationStrategy DSL extension can be used to include an Aggregator into Camel routes. It is implemented with the org.openehealth.ipf.platform.camel.core.adapter.AggregatorAdapter that translates between org.openehealth.ipf.commons.core.modules.api.Aggregator and org.apache.camel.processor.aggregate.AggregationStrategy. The aggregationStrategy extension supports an aggregator object, a bean name or a closure as argument. The created Camel AggregationStrategy can then be used with e.g. enrich or other DSL elements that expect an org.apache.camel.processor.aggregate.AggregationStrategy. The aggregationStrategy extension is provided by an IPF extension of the org.apache.camel.spring.SpringRouteBuilder. Here are some examples:

'aggregationStrategy' extension
def aggregationStrategy1 = aggregationStrategy(new TestAggregator())
def aggregationStrategy2 = aggregationStrategy('sampleAggregator') 
def aggregationStrategy3 = aggregationStrategy {originalInBody, resourceOutBody -> 
    originalInBody + ':' + resourceOutBody
} 
def aggregationStrategy4 = aggregationStrategy {originalInBody, resourceOutBody, fooHeader -> 
    originalInBody + ':' + resourceOutBody  + ':' + fooHeader
}
.input            {originalExchange -> originalExchange.in.body}        // relates to 1st parameter
.aggregationInput {resourceExchange -> resourceExchange.out.body}       // relates to 2nd parameter
.params           {originalExchange -> originalExchange.in.headers.foo} // relates to 3rd parameter

from('direct:input1').enrich('direct:resource', aggregationStrategy1).to('mock:output')
from('direct:input2').enrich('direct:resource', aggregationStrategy2).to('mock:output')
from('direct:input3').enrich('direct:resource', aggregationStrategy3).to('mock:output')
from('direct:input4').enrich('direct:resource', aggregationStrategy4).to('mock:output')

The collection passed to the org.openehealth.ipf.commons.core.modules.api.Aggregator.zap method is a list of two objects - objects that have been derived from the arguments to the org.apache.camel.processor.aggregate.AggregationStrategy.aggregate method. If you use a closure you must define at least two parameters that correspond to the parameters of org.apache.camel.processor.aggregate.AggregationStrategy.aggregate. An optional third parameters corresponds to the params parameter that is common to all modules interfaces. Input can be customized via the input, params or staticParams DSL extensions. Input for the second object in the input list or the second closure parameter can be customized via the aggregationInput DSL extension (see aggregationStrategy4 in the example above).

Adapter extension summary

Relevant types
DSL extension Modules Interface *) Adapter class **) Extended model class
transmogrify Transmogrifier TransmogrifierAdapter org.apache.camel.model.ProcessorDefinition
validate Validator ValidatorAdapter org.apache.camel.model.ProcessorDefinition
parse Parser ParserAdapter org.apache.camel.model.ProcessorDefinition
org.apache.camel.builder.DataFormatClause
render Renderer RendererAdapter org.apache.camel.model.ProcessorDefinition
org.apache.camel.builder.DataFormatClause
predicate Predicate PredicateAdapter org.apache.camel.spring.SpringRouteBuilder
aggregationStrategy Aggregator AggregatorAdapter org.apache.camel.spring.SpringRouteBuilder

*) in package org.openehealth.ipf.commons.core.modules.api
**) in package org.openehealth.ipf.platform.camel.core.adapter

Parameters and input
DSL extension Parameter types Input customization
transmogrify
  • Transmogrifier
  • java.lang.String (bean name)
  • groovy.lang.Closure
  • input
  • params
  • staticParams
validate
  • Validator
  • java.lang.String (bean name)
  • groovy.lang.Closure
  • input
  • profile
parse
  • Parser
  • java.lang.String (bean name)
  • input
  • params
  • staticParams
render
  • Renderer
  • java.lang.String (bean name)
  • input
  • params
  • staticParams
predicate
  • Predicate
  • java.lang.String (bean name)
  • groovy.lang.Closure
  • input
  • params
  • staticParams
aggregationStrategy
  • Aggregator
  • java.lang.String (bean name)
  • groovy.lang.Closure
  • input
  • aggregationInput
  • params
  • staticParams
Closure profiles
DSL extension Parameter 1 Parameter 2 Parameter 3 Return value
transmogrify
  • Default value: in-message body
  • Customization via: input
  • Mandatory: true
  • Default value: in-message headers
  • Customization via: params, staticParams
  • Mandatory: false
- Transformation result (any type)
validate
  • Default value: in-message body
  • Customization via: input
  • Mandatory: true
  • Default value: null
  • Customization via: profile, staticProfile
  • Mandatory: false
- boolean or throws ValidationException
predicate
  • Default value: in-message body
  • Customization via: input
  • Mandatory: true
  • Default value: null
  • Customization via: params, staticParams
  • Mandatory: false
- boolean
aggregationStrategy
  • Default value: in-message body of oldExchange
  • Customization via: input
  • Mandatory: true
  • Default value: out-message body of newExchange
  • Customization via: aggregationInput
  • Mandatory: true
  • Default value: null
  • Customization via: params, staticParams
  • Mandatory: false
aggregation result (any type)

DSL extensions for Groovy XML processing

IPF provides support for Groovy XML processing within Camel routes. Here's a summary of features

XML Unmarshalling with Groovy XmlParser

To unmarshal an XML stream or string using a groovy.util.XmlParser use Camel's unmarshal method with IPF's gnode extension.

'gnode' extension for unmarshalling (namespace-aware)
from('direct:input1')
    .unmarshal().gnode()
    .transmogrify { node ->
        // process XML ...
    }

This puts the parser result into the message body which can then be used in subsequent processors. By default, gnode is namespace-aware. To disable namespace-awareness use gnode(false).

'gnode' extension for unmarshalling (namespace-unaware)
from('direct:input1')
    .unmarshal().gnode(false)
    .transmogrify { node ->
        // process XML ...
    }

As of IPF 1.7, in order to add XML schema validation, simply add a schema location parameter. The value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource:

'gnode' extension for unmarshalling (namespace- and schema-aware)
from('direct:input1')
    // will throw an Exception if XML does not validate against the schema
    .unmarshal().gnode('path/to/schema.xsd', true) 
    .transmogrify { node ->
        // process XML ...
    }

XML Unmarshalling with Groovy XmlSlurper

To unmarshal an XML stream or string using a groovy.util.XmlSlurper use Camel's unmarshal method with IPF's gpath extension.

'gpath' extension for unmarshalling (namespace-aware)
from('direct:input1')
    .unmarshal().gpath()
    .transmogrify { gpathResult ->
        // process XML ...
    }

This puts the slurper result into the message body which can then be used in subsequent processors. By default, gpath is namespace-aware. To disable namespace-awareness use gpath(false).

'gpath' extension for unmarshalling (namespace-unaware)
from('direct:input1')
    .unmarshal().gpath(false)
    .transmogrify { gpathResult ->
        // process XML ...
    }

As of IPF 1.7, in order to add XML schema validation, simply add a schema location parameter. The value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource:

'gpath' extension for unmarshalling (namespace- and schema-aware)
from('direct:input1')
    // will throw an Exception if XML does not validate against the schema
    .unmarshal().gpath('path/to/schema.xsd', true) 
    .transmogrify { node ->
        // process XML ...
    }

XML Marshalling with Groovy XmlNodePrinter

The reverse operations to unmarshal().gnode() and unmarshal().gpath() are marshal().gnode() and marshal().gpath(), respectively. Currently only marshal().gnode() is supported. This writes a groovy.util.Node to an output stream using Groovy's groovy.util.XmlNodePrinter

'gnode' extension for marshalling (namespace-aware)
    ...
    .marshal().gnode()
    .to('mock:mock')

This makes the printer result available as byte array in the message body. By default gnode is namespace-aware. To disable namespace-awareness use gnode(false).

'gnode' extension for marshalling (namespace-unaware)
    ...
    .marshal().gnode(false)
    .to('mock:mock')

XML transmogrifiers

IPF provides an easy way to make Groovy XML markup builders available inside transmogrifiers or transmogrifier closures. Here's how it works for transmogrifier closures.

XML transmogrifier
from('direct:input1')
    .transmogrify { body, xmlBuilder ->
        // use xmlBuilder to create XML
        ...
        // return xmlBuilder result
        xmlBuilder.result
    }
    .params().builder()

The params().builder() call makes the builder available via the second closure parameter. A new instance of the builder is passed with every call, so using that builder is thread-safe. If you want to have both, the in-message headers and the builder, for the second closure parameter then use the predefined headersAndBuilder() extension. This extension creates an array of length 2 where the first array element is the in-message headers and the second element is the XML builder.

XML transmogrifier
from('direct:input1')
    .transmogrify { body, params ->
        def msgHeaders = params[0]
        def xmlBuilder = params[1]
        // use builder to create XML
        ...
        // return builder result
        xmlBuilder.result
    }
    .params().headersAndBuilder()

The xmlBuilder.result property was added by a class that subclasses groovy.xml.MarkupBuilder. It contains the result XML document as String.

XML message transmformations

By combining params().builder() with unmarshal().gnode() or unmarshal().gpath() transmogrifiers can implement XML message transformations based on Groovy's XML support. Transformation logic usually extracts content from an XML source document and includes that content into a result document. Navigation and content extraction from source documents is done via GPath expressions. Creation of result documents is done with Groovy's XML markup builder. The following example shows how an XML transformation route can be set up.

from(...)
    .unmarshal().gpath()
    .transmogrify { gpathResult, xmlBuilder ->
        // use gpathResult to read from source XML
        // use xmlBuilder to create result XML
        ...
        // return builder result
        xmlBuilder.result
    }
    .params().builder()
    .to(...)
{code}

In most cases creating a result document using an XML builder goes over a large number of lines of code and you probably want to factor that code out to a Transmogrifier that is implemented in Groovy. Here's an example.

MyTransmogrifier.groovy
public class MyTransmogrifier implements Transmogrifier {

     Object zap(Object input, Object... params) {
         // obtain builder from params
         def builder = params[0]
         // create XML document using builder
         ...
         // return result
         builder.result
     }

}

Such a Transmogrifier implementation can then be included into Camel routes either directly as transmogrifier object or via a Spring bean name.

XML transmogrifier object
from('direct:input1')
    .transmogrify(new MyTransmogrifier())
    .params().builder()
XML transmogrifier bean
from('direct:input1')
    .transmogrify('myTransmogrifierBean')
    .params().builder()

Mapping Service

Overview

The org.openehealth.ipf.commons.map.MappingService interface deals with the requirement that message processing often involves mapping 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 and java.util.Collection classes with methods targeted at mapping.

The commons-map library provides one MappingService implementation (org.openehealth.ipf.commons.map.BidiMappingService), which implements

  • bidirectional mapping
  • mapping of arbitrary objects
  • definitions of mappings using external Groovy Scripts

Configuring the Mapping Service

This section explains how to configure IPF's BidiMappingService.

Using the BidiMappingService bean

  1. Add the necessary dependencies to your project's Maven 2 descriptor.
    pom.xml
    ...
    <!-- Dependency for accessing and manipulating HL7 v2 structures -->
    <dependency>
        <groupId>org.openehealth.ipf.commons</groupId>
        <artifactId>commons-map</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    ...
    


  2. To use BidiMappingService, define a Spring bean and initialize it with the external Groovy resource:
    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://camel.apache.org/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://camel.apache.org/schema/spring
    http://camel.apache.org/schema/spring/camel-spring.xsd">
    
    ...
    <!-- Groovy class that provides the operations on the mappings -->
    <bean id="myMappingService" class="org.openehealth.ipf.commons.map.BidiMappingService">
       <property name="mappingScript" value="classpath:example.groovy"/>
    </bean>
    ...
    
  3. Register the Mapping Service extensions in the Spring Application Context of your application
    context.xml
    
    ...
    
        <!-- Mapping extensions -->
        <bean id="mappingExtension" 
            class="org.openehealth.ipf.commons.map.extend.MappingExtension">
            <property name="mappingService" ref="myMappingService" />
        </bean>
    
        <!-- Register the extensions -->
        <bean id="routeModelExtender"
            class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
            <property name="routeModelExtensions">
                <list>
                    ...
                    <ref bean="mappingExtension" />
                </list>
            </property>
        </bean>
    
        ...
    
    </beans>
    

Using the BidiMappingServiceConfigurer bean

The BidiMappingServiceConfigurer supports distributed configurations of a BidiMappingService instance. This is useful when several components want to contribute mappings to a shared mapping service. Each component defines a BidiMappingServiceConfigurer bean in its application context that references the shared mapping service.

context-main.xml
<bean id="sharedMappingService" 
    class="org.openehealth.ipf.commons.map.BidiMappingService">
</bean>
context-component1.xml
<bean id="bidiMappingServiceConfigurer1" 
    class="org.openehealth.ipf.commons.map.BidiMappingServiceConfigurer">
    <property name="mappingService" ref="sharedMappingService" />
    <property name="mappingScript" value="configurer1.map" />
</bean>
context-component2.xml
<bean id="bidiMappingServiceConfigurer2" 
    class="org.openehealth.ipf.commons.map.BidiMappingServiceConfigurer">
    <property name="mappingService" ref="sharedMappingService" />
    <property name="mappingScripts">
        <list>
            <value>configurer2.map</value>
            <value>configurer3.map</value>
        </list>
    </property>
</bean>

Definition of Mappings

A mapping example is displayed below. The example maps a couple of codes from HL7-related code systems (see HL7 Messaging for more details on IPF's HL7 support).

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 ISO Object Identifiers (OIDs) to identify key and value code systems. The encounterType mapping has three entries, while the vip and messageType mappings have only one.

You can use the mappingService directly, or you can take advantage of the MappingExtension:

example.groovy
   def x = mappingService.get('encounterType', 'E') // using the service bean reference
   def y = 'E'.map('encounterType')   // more concise: using the dynamic map method
   def z = 'E'.mapEncounterType()     // even more concise
   // x == y == z == 'EMER'

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 Exception is thrown.

The services also allow mapping in the backward direction:

example.groovy
   def x = mappingService.getKey('vip', 'VIP')  // Y
   def y = 'VIP'.mapReverse('vip')              // Y
   def z = 'VIP'.mapReverseVip()                // 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="myMappingService" class="org.openehealth.ipf.commons.map.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.

Furthermore, BidiMappingService 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.

HL7 v2 Messaging

This chapter describes how IPF facilitates HL7 v2 messaging.


HL7 v2 Messaging Overview

HL7's Version 2.x messaging standard is the workhorse of electronic data exchange in the clinical domain and arguably the most widely implemented standard for healthcare in the world. There have been seven releases of the Version 2.x Standard to date.

The HL7 Standard covers messages that exchange information in the general areas of Patient Demographics, Patient Charges and Accounting, Clinical Observations, Medical Records Document Management, and many more.

HL7 Version 2.6 represents HL7's latest development efforts to the line of Version 2 Standards that date back to 1989.

Features

IPF's HL7 v2 support does not reinvent the wheel. It leverages HAPI (http://hl7api.sourceforge.net), one of the most proven HL7 v2 Java libraries. It provides, however, features on top of HAPI that adds a lot of convenience compared to the original API, and retrofits some missing items.

Feature Functionality See
HL7v2 DSL A domain specific language based on the Groovy programming language for manipulating HL7 messages. HL7 message processing in IPF applications becomes almost trivial. HL7 DSL
Extended HL7 Parser classes and Factories For more flexibility in defining valid sets of HL7 structures Extensions to HAPI
Convenient creation of HL7 messages API for creating new messages and responses, in particular HL7 acknowledgements. Extensions to HAPI
HL7 v2 Validation API A specialized DSL dedicated to defining validation rules for HL7 messages HL7 Message Validation
Camel adapters Camel data types for using the HL7 v2 DSL and Validation inside Camel integration routes. Camel DSL Extensions for HL7

Configuring HL7 v2 Messaging

This section explains how to configure IPF's HL7 messaging support.

Make sure you already have correctly set up

  • Maven 2.0.9 or better
  • a possibly empty IPF project

After configuration you will be able to use the HL7 v2 related DSLs in a standalone scenario.

  1. Add the necessary dependencies to your project's Maven 2 descriptor.
    pom.xml for standalone scenario
    ...
    <!-- Dependency for accessing and manipulating HL7 v2 structures -->
    <dependency>
        <groupId>org.openehealth.ipf.modules</groupId>
        <artifactId>modules-hl7dsl</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    
    <!-- Dependency for extending the API of the HAPI HL7 library -->
    <dependency>
        <groupId>org.openehealth.ipf.modules</groupId>
        <artifactId>modules-hl7</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    ...
    


  2. Depending on the HL7 v2 versions, add the corresponding dependencies to the HAPI library. HAPI supports versions 2.2 through 2.6.
    pom.xml
    ...
    <!-- Dependency for HL7 v2.5 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v25</artifactId>
        <version>0.6</version>
    </dependency>
    
    <!-- Dependency for HL7 v2.5.1 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v251</artifactId>
        <version>0.6</version>
    </dependency>
    ...
    


  3. Register the HL7 extensions in the Spring Application Context of your application
    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://camel.apache.org/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://camel.apache.org/schema/spring
    http://camel.apache.org/schema/spring/camel-spring.xsd">
    
        <!-- Setting up the Camel context -->
    
        <camel:camelContext id="camelContext">
           <camel:routeBuilder ref="routeBuilder" />
        </camel:camelContext>
    
        <bean id="routeBuilder" depends-on="routeModelExtender" class="..." />
    
        ...
    
        <!-- HAPI extensions -->
        <bean id="hapiModelExtension" 
            class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
            <property name="mappingService" ref="..." />
        </bean>
    
        <!-- Register the extensions -->
        <bean id="routeModelExtender"
            class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
            <property name="routeModelExtensions">
                <list>
                    ...
                    <ref bean="hapiModelExtension" />
                </list>
            </property>
        </bean>
    
        ...
    
    </beans>
    

HL7 v2 DSL

This section gives a detailed introduction to the Groovy-based HL7 v2 domain specific language.

The HL7 v2 DSL provides a unique programming interface for handling HL7 messages. Its API aligns very closely with natural language and the syntax of HL7 v2 as often seen in specifications and requirements. You don't need to translate anymore from the language of the "HL7 world" into the language of the "developer's world".
The DSL can be subdivided into the following groups of functionality:

  • Construction: copying or loading messages from file or a plain string
  • Navigation: accessing HL7 v2 substructures like groups, segments, or fields
  • Manipulation: assigning new values to HL7 structures
  • Rendering: writing a message or parts thereof to their external representation

For the purpose of demonstrating the DSL, a ORU_R01 message of HL7 v2.5 is taken as example.

Construction

Use load to construct a message from an HL7 file on the classpath or from an InputStream.

import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.*

def message = load('oru-r01-25.hl7')

Alternatively, the message 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 easily create a message as a copy of an existing message:

def messageCopy = message.copy()
Anonymous types

Groovy doesn't require to specify the exact type of a variable, instead you can use the def keyword. For HL7 v2 processing, this is a very convenient feature that saves you many explicit type checks and type casts.

The message object that is constructed is a org.openehealth.ipf.modules.hl7dsl.MessageAdapter object that wraps the original HAPI message object. The complete HL7 v2 DSL only works with MessageAdapter objects. If you have a native HAPI message object, you can wrap it manually:

ca.uhn.hl7v2.model.Message hapiMessage = ....
MessageAdapter message = new MessageAdapter(hapiMessage)

Navigation

The DSL offers a position-based navigation of HL7 structures and fields. It's all valid Groovy Syntax, accomplished by operator overloading and metaclass programming, so you don't need a intermediate step that parses the expressions (cf. the Terser class in HAPI.)

Navigation to groups and segments

Groups and Segments can be accessed by name like an object property.

def msh   = message.MSH               // Obtain the Message Header Segment

def group = message.PATIENT_RESULT(0) // Obtain the first PATIENT_RESULT group
def pid   = group.PATIENT.PID         // Obtain the PID segment contained inside the PATIENT group.            

Note that although the HL7 DSL hides much of the technical details and APIs, you still require profound knowledge of the HL7 specifications when working with HL7 messages. In the example above, e.g. you need to know that for ORU_R01 structures the PID segment is nested inside two groups.

Navigation to fields

Obtaining fields is similar to obtaining structures except that fields are often referred to by number instead of by name. Fields are accessed like an array field; components in a composite field are accessed like a two-dimension array:

def composite = message.MSH[3]     // MSH-3 = sending application composite field
def primitive = message.MSH[3][2]  // MSH-3-2 = universal ID primitive field

It's also possible to navigate by specifying the field names instead of the number.

def primitive = message.MSH.sendingApplication.universalIDType

Take care, however, that along with the change of internal message structures, individual field names change between HL7 versions although they refer to the same position of the field in a segment. If you don't know the version of the HL7 message in advance, better use the more concise index notation. Example:

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

Field values

Field variables render to their string encoding e.g. when printed, by implementing an appropriate toString() method. However, for literal comparison or variable assignment you
use the value property to obtain the value of a primitive field.

String primitiveValue = message.MSH[3][2].value
primitiveValue = message.MSH[3][2].toString()
if ("xyz".equals(message.MSH[3][2].value)) {
   ...
}

HL7 Null Values

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()

Repetitions

Groups, Segments and Fields may repeat. Use parentheses like with regular method calls in order to obtain a certain element of a repeating structure. The next example shows how to navigate in a nested repetitive structure.

def group = message.PATIENT_RESULT(0).PATIENT  // access first PATIENT_RESULT group
def nk1   = group.NK1(0)                       // access first NK1 segment
def phone = nk1[5](0)                          // access first NK1-5 field (phone)

To get a list of elements of a repeating structure, simply omit the index so that it looks like a method call without parameters.

def phones = nk1[5]()  // returns a list of phone elements

Furthermore, repetitions can be counted:

def count = nk1.count(5)      // returns size of the phone number list

Smart navigation

Navigating HL7 messages as described above usually requires knowledge about the specified message structure, which is often not visible by looking at the printed message:

def nk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0) // Repetitions of groups or segments:
def phoneNumber = nk1[5](0)[1]                     // Repetition of fields
def familyName  = nk1[2][1][1]                     // Name is first component of FN composite type

To make things worse, the internal structure changes between HL7 versions. In higher versions, primitive fields are sometimes replaced with composite fields, having the so far used primitive as first component. This appears to be backwards compatible on printed messages, but requires different DSL expressions when obtaining field values.

Smart navigation resolves these problems by assuming reasonable defaults when repetitions or component operators are omitted:

  • If a repetition operator () is omitted, the first repetition of a group, segment or field is assumed
assert message.PATIENT_RESULT(0).PATIENT == message.PATIENT_RESULT.PATIENT // group
assert group.NK1(0)[5](0)[1].value == group.NK1[5](0)[1].value             // segment
assert group.NK1(0)[5](0)[1].value == group.NK1[5][1].value                // field
  • If a component is omitted, the first component or subcomponent of a composite is assumed
assert group.NK1(0)[5](0)[1].value == group.NK1[5].value
assert group.NK1(0)[2][1][1].value == message.NK1[2].value
  • Consequently, Smart Navigation also works with HL7 Null values:
assertEquals ''  , pid[11][1][1].value           // 'full' expression
assertEquals '""', pid[11][1][1].originalValue
assertTrue pid[11][1][1].isNullValue()
assertEquals ''  , pid[11].value                 // 'smart' expression
assertEquals '""', pid[11].originalValue
assertTrue pid[11].isNullValue()

Using smart navigation, the navigation expressions are usually shorter and less error-prone. Furthermore, in many cases the same expressions can be used for different HL7 versions that define new structures in a backward-compatible way.

Access target objects

Objects of the HAPI DSL layer internally reference objects defined in the HAPI ca.uhn.hl7v2.model package. These can be accessed via the target property.

ca.uhn.hl7v2.model.Segment hapiSegment = nk1.target

However, this is usually not needed because any property access or method call not applicable to HAPI DSL model objects is forwarded to target objects.

int cardinality1 = message.NK1(0).target.getMaxCardinality(3)
int cardinality2 = message.NK1(0).getMaxCardinality(3)         // equivalent
String segmentName1 = message.NK1(0).target.name
String segmentName2 = message.NK1(0).name                      // equivalent

Typically you need to reference the target object e.g. when passing control to code that is unaware of the HL7 DSL or explicitly requires HAPI classes.

Group and Segment emptyness (as of IPF 2.1)

Emptyness for segments and groups is defined as follows:

  • a segment is empty if all fields are empty
  • a group is empty if all contained groups and segments are empty

For brevity, the GroupAdapter and SegmentAdapter classes both implement an isEmpty() method.

assert msg1.PATIENT_RESULT.PATIENT.PV1.isEmpty() == false // not empty because some fields are filled
assert msg1.PATIENT_RESULT.PATIENT.PV2.isEmpty() == true  // empty because all fields are empty
assert msg1.PATIENT_RESULT.isEmpty() == false             // non empty because it contains a non-empty group

Iterative functions (as of IPF 2.1)

As HL7 messages are compound structures, you can imagine to iterate over them. Thus, the HL7 DSL implements iterators for HL7 messages and groups. Due to their nested structures, iteration is implemented as a depth first traversal over all non-empty substructures, i.e. non-empty groups and segments (see previous section).

An iterator() function is defined for the GroupAdapter and MessageAdapter classes. You seldomly will use iterator() directly, however, a lot of Groovy's iterative functions only rely on the existence of an iterator function. As a consequence, you can e.g. use the following Groovy functions on HL7 messages and groups:

  • each
  • eachWithIndex
  • every
  • any
  • collect
  • find
  • findAll
  • split
  • for statement
  • the spread operator

Some examples:

// Count the number of substructures
int numberOfStructures = 0
msg1.each { numberOfStructures++ }
println "The message has $numberOfStructures substructures"

// Check if there are any groups
boolean hasGroups = msg1.any { it instanceof GroupAdapter }

// A list of the names of all substructures
def names = msg1*.name

// For loop
for (def structure in msg1) {
  // do something with structure
}

// Find the first nested OBX segment
def obx = msg1.find { it.name == 'OBX' }
obx = msg1.findOBX() // shortcut notation

// Find all nested OBX segments
def obxList = msg1.findAll { it.name == 'OBX' }
obxList = msg1.findAllOBX() // shortcut notation

The find/findAll methods are handy in the following use cases:

  • accessing data in a deeply nested message structure that is not visible in the pipe-encoded representation.
  • uniformly accessing corresponding fields in messages with different structure
  • messages that have a group structure in a newer HL7 version while having a flat structure in previous versions.
def patientName = msg1.PATIENT_RESULT(0).PATIENT.PID[5][1].value
patientName = msg1.findPID()[5][1].value  // equivalent, shorter, and group-structure-agnostic

Manipulation

Message manipulation is as straightforward as navigation. You navigate to a segment or field and assign it a new object.

Manipulating segments

Currently you can change segments only, assignment to groups isn't supported yet.

msg1.EVN = msg2.EVN     // copy over EVN segment from msg2 to msg1
msg1.EVN.from(msg2.EVN) // equivalent

There's a dedicated method nrp(index) available for adding a repetitions to a repeating field

def newField = message.PATIENT_RESULT(0).PATIENT.NK1(0)[5].nrp(5) // Adds a repetition to NK1[5]

There are two caveats:

First, segments are copied with the assignment (i.e. =) operator only if the assignment operator follows a property read-access operation (via .property or ['property']). If you make an assignment directly to a segment variable, you assign object references.

def mySegment = ...
message.EVN = mySegment          // mySegment copied into message.EVN

def targetSegment = message.EVN
targetSegment = mySegment        // message.EVN remains unchanged, 
                                 // targetSegment and mySegment reference the
                                 // same object

Second, when you obtain a segment from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.

def mySegment = ...              // assignment to another NK1 segment instance
def group = message.PATIENT_RESULT(0).PATIENT
group.NK1(0) = 'abc'             // syntax error!
msg1.NK1(0) = mySegment          // syntax error!
msg1.NK1(0).from(mySegment)      // works!

Manipulating fields

To change a field value, navigate to the field (either by name or index, as shown above) and either assign it a string value or another field. Fields may also be changed by using the from() method.

def nk1      = message.PATIENT_RESULT(0).PATIENT.NK1(0)
def otherNk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0)
nk1[4]       = otherNk1[4]       // copy address
nk1[4][4]    = otherNk1[4][4]    // copy state or province only
nk1[4][4].from(otherNk1[4][4])   // equivalent

nk1[4][4]    = 'NY'              // set state or province directly

There are the same caveats with manipulating fields as with manipulating segments:
First, Composites are copied with the assignment (i.e. =) operator only if the assignment operator follows a subscript (i.e. []) operation. If you make an assignment to a composite variable directly you assign the respective object references

def otherXad = ...      // a contact address = HL7 composite type XAD
def nk1 = group.NK1(0)
nk1[4] = otherXad       // otherXad copied into nk1[4]

def xad = nk1[4]
xad = otherXad          // nk1[4] remains unchanged, xad and otherXad reference 
                        // the same object

Second, when you obtain a field from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.

def field = ...                // a primitive or composite field
def other = ...                // a primitive or composite field to be assigned
field(0) = 'abc'               // syntax error!
field(0) = other               // syntax error!
field(0).from(other)           // works for primitives and composites
field(0).value = 'abc'         // works for primitives only

Adding repetitions

Repetitions occur in HL7 groups, segments and fields. When creating a new message or manipulating an parsed message, it may become necessary to add a repeating element. A good example is the ORU_R01 message in HL7 v2.5, which includes nested repeatable groups, which in return contain repeatable segments that have repeatable fields.

There are two ways to add a repeating element: explicitly and implicitly.
Explicitly calling nrp() (for "new repetition") adds an element and returns it to the caller. The argument is of type String for repeating structures or int for repeating fields:

def message       = new MessageAdapter(new ORU_R01())
def patientResult = message.nrp('PATIENT_RESULT')          // add a PATIENT_RESULT group
def order         = patientResult.nrp('ORDER_OBSERVATION') // add a ORDER_OBSERVATION group
def observation   = order.nrp('OBSERVATION')               // add a OBSERVATION group
def obx5          = observation.OBX.nrp(5)                 // add a OBX-5 field

For consistency with HAPI, an element is also added if you access a repetition that does not exist yet.

def message       = new MessageAdapter(new ORU_R01())
def patientResult = message.PATIENT_RESULT(0)          // add a PATIENT_RESULT group
def order         = patientResult.ORDER_OBSERVATION(0) // add a ORDER_OBSERVATION group
def observation   = order.OBSERVATION(0)               // add a OBSERVATION group
def obx5          = observation.OBX[5](0)              // add a OBX-5 field
Index out of bounds!

The DSL does not distinguish whether the new repetition would be the next one to be created or not. If there's no PATIENT_RESULT group in the message, then msg.PATIENT_RESULT(8) does not silently add seven empty groups and returns the eighth! Instead only one group is added and returned, i.e. you actually obtain msg.PATIENT_RESULT(0).

Together with the Smart Navigation feature, it is particularly convenient that accessing a repeated element without index does a default to its first repetition. Hence, the code above can be condensed to:

def message = new MessageAdapter(new ORU_R01())
def obx5    = message.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION.OBX[5]

Rendering

Rendering writes the internal representation of a HL7 v2 message to its external representation, which is usually the ER7-encoded form with pipe field seperators.

To write a message to stdout, messages can be written to stream using the left-shift (<<) operator.

System.out << message

Otherwise, using the message variable in a string context or explicitly calling toString() does the same job:

assert message.toString() == "${message}"

Functional Extensions to HAPI

While the HL7 v2 DSL has its focus on providing a domain-specific syntax to navigate in HL7 messages and changing fields within messages, the functional extensions retrofit a couple of convenient functions on top of HAPI. By means of Groovy metaprogramming, however, it looks like these extensions are part of the HAPI API, i.e. you can call the methods on both the raw HAPI objects and the wrapper objects invisibly added by the HL7 v2 DSL.

It's important to note that the HL7 v2 DSL and the functional extensions do not depend on each other - you can employ any one or both feature sets as you like.

HL7 PipeParser and custom 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, there remains a lack of flexibility, e.g. it's not possible to use two distinct sets of "dialects" within one Java process. 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)

If you use a custom model class factory, it's strongly recommended to provide the factory instance to the HapiModelExtension extension class. This ensures that the various extensions that create HL7 message or structures (see below for details) use this factory to create the HL7 objects.

context.xml
...

    <!-- A custom model for HL7 v2.5 message -->
    <bean id="myModelClassFactory" class="org.openehealth.ipf.modules.hl7.parser.CustomModelClassFactory">
        <property name="customModelClasses">
            <map>
                <entry key="2.5">
                    <list>
                        <value>com.mycompany.profile1.hl7def.v25</value>
                        <value>com.mycompany.profile2.hl7def.v25</value>
                    </list>
                </entry>
                ....
            </map>
        </property>
    </bean>

...

    <!-- HAPI extensions -->
    <bean id="hapiModelExtension" 
        class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
        <property name="mappingService" ref="..." />
        <property name="factory" ref="myModelClassFactory"/>
    </bean>

...

Custom PipeParser

The PipeParser implementation provided by this module (org.openehealth.ipf.modules.hl7.parser.PipeParser) by default uses the CustomModelClassFactory and a default set of Primitive validation rules (see HL7 v2 Message Validation chapter for details). Apart from that it does not add any features to the HAPI PipeParser class.

import org.openehealth.ipf.modules.hl7.parser.PipeParser;

...
def customParser = new PipeParser(customFactory)

Methods added to the HAPI Message interface

New Messages

You can create a new message from scratch by specifying event type, trigger event and version. Its message header fields are populated with the event type, trigger event, version, the current time as message date, and the common separators.

import ca.uhn.hl7v2.model.Message

// Static method extension to the HAPI Message class
def msg = Message.ADT_A01('2.5') // creates a ca.uhn.hl7v2.model.v25.message.ADT_A01 object
HL7 Message Structures

The message structure is a data structure that expresses an association of a message type with an event for a class of HL7 messages. Each message structure also contains a unique ID, e.g. ADT_A01.
Beginning with HL7 v2.4, the combination of event type and trigger event is NOT necessarily the message structure ID, e.g. a ADT^A04 message in version 2.5 has the message structure ADT_A01.
Note that HAPI message classes are in fact message structure classes, i.e. to correctly create a ADT^A04 v2.5 message, you need to instantiate an object of class ca.uhn.hl7v2.model.v25.message.ADT_A01 and set the MSH-9 field to ADT^A04^ADT_A01.

When you create new messages using IPF's HAPI extensions, the message structure (and therefore the HAPI class to be used) is automatically derived from event type, trigger event and message version, and MSH-9 is populated accordingly. Example:

import ca.uhn.hl7v2.model.Message

def msg = Message.ADT_A04('2.5')
assert msg instanceof ca.uhn.hl7v2.model.v25.message.ADT_A01

Acknowledgements and Responses

You can create positive or negative acknowledgments to HL7 messages with a single method call. The acknowledgment message

  • is in the same HL7 version as the original message
  • refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
  • contains the current timestamp as message date
  • is populated with MSA and/or ERR segments as specified in the parameters.
// Positive Acknowledgement
def ack  = msg.ack()
// Negative Acknowledgements
def nak1 = msg.nak('Reason for failure', AckTypeCode.AE)
def nak2 = msg.nak(new HL7Exception('reason for failure', 204), AckTypeCode.AE)

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.

def nak = ca.uhn.hl7v2.model.Message.defaultNak(e, AckTypeCode.AE, '2.5') // NAK of version 2.5

Generating acknowledgments is only a special case of generating a response to an original message. If a response is defined as dedicated HL7 message as with responses to Query messages, you have to use the respond(eventType, triggerEvent) extension method. The response message

  • is in the same HL7 version as the original message
  • refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
  • contains the current timestamp as message date
  • has a populated MSA segment
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message

Message checks

Use the matches extension method to 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
}

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

Message dump

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 you should avoid using this in production environments:

println msg.dump()

Methods added to the HAPI Structure interface

New segments

Just as creating a message, you can also create a segment by calling its respective name as static method on the ca.uhn.hl7v2.model.Structure interface. You need to pass the enclosing Message object as argument, which determines the HL7 version to be used.

import ca.uhn.hl7v2.model.Segment
...
// Static method extension to the HAPI Message class
def obx = Segment.OBX(msg) // creates a ca.uhn.hl7v2.mode.v25.segment.OBX object
//  obx = Segment.OBX(msg.target) if msg is a MessageAdapter

Printing structures

All HAPI Structures (i.e. not only Messages, but also arbitrary Groups and Segments) can be converted into their pipe-encoded representation by calling the encode() extension method. 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

Methods added to the HAPI Type interface

New fields

Just as creating a message or segment, you can also create a field by calling its respective name as static method on the ca.uhn.hl7v2.model.Composite or ca.uhn.hl7v2.model.Primitive}}interface. You need to pass the enclosing {{Message object as argument, which determines the HL7 version to be used.
Composites may be initialized with a map containing the component values. Primitives may be initialized with a literal string value.

import ca.uhn.hl7v2.model.Composite
import ca.uhn.hl7v2.model.Primitive
...
// Static method extension to the HAPI Composite class
def ce = Composite.CE(msg, [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM'])
//  ce = Composite.CE(msg.target, ...) if msg is a MessageAdapter
// Static method extension to the HAPI Primitive class
def st = Primitive.ST(msg, 'value')
//  st = Primitive.ST(msg.target, 'value') if msg is a MessageAdapter

Printing types

All HAPI Types (i.e. Primitives, Composites, and Varies) can be converted into their pipe-encoded representation by calling the encode() extension.

assert message.MSH.messageType.encode() == 'ADT^A01'

// Together with the HL7 DSL, you can also write
assert message.MSH[9].encode() == 'ADT^A01'

Mapping Service

The Mapping Service has been moved to the IPF Core features. After all, although often used in HL7 processing, code system mapping is not a feature that is inherently exclusive for HL7. Please refer to the Mapping Service chapter.
What remains specific to IPF's HL7 v2 support, however, is that the mapping extensions can be applied directly on all HAPI types. The encode() extension is called before the mapping is executed.

Given the following mapping example:

example.map
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) }
                 )
}

You can use the mapping functions directly on composite or primitive field objects:

// 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 v2 DSL, you 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'

Examples for HL7 Messaging

This section shows the HL7 v2 DSL and Functional Extensions to HAPI in action. As the primary purpose of DSL and extensions is to augment the original HAPI library, it seems appropriate to take HAPI examples code and reimplement them.

Create a message from scratch

The first example is to create a simple message from scratch, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/CreateAMessage.html.

The example shows

  • how to use Groovy properties instead of getter methods for named access to fields
  • how to use IPF's functional extension to create messages with a prefilled MSH segment
  • how to employ Groovy's with clause.
CreateAMessage.groovy

import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension
import org.openehealth.ipf.modules.hl7.parser.PipeParser
import ca.uhn.hl7v2.model.*

public class CreateAMessage{
        
    static def makeMessage() {
        def msg = Message.ADT_A01('2.4')
        // 'with' is a Groovy feature that delegate all unknown method properties
        // in this case to the msg.MSH object.
        msg.MSH.with {
            sendingApplication.namespaceID.value = 'TestSendingSystem' 
            sequenceNumber.value = '123'
        }
        msg.PID.with {
            getPatientName(0).familyName.surname.value = 'Doe'
            getPatientName(0).givenName.value = 'John'
            getPatientIdentifierList(0).ID.value = '123456'
        }
        println "Printing ER7 Encoded Message:"
        println new PipeParser().encode(msg)
    }
    
    /**
     * @param args
     */
    public static void main(def args) {
        // Initialize the MetaClass extension
        ExpandoMetaClass.enableGlobally()
        new HapiModelExtension().extensions.call()
        // 
        makeMessage()
    }
}

The console output is:

Printing ER7 Encoded Message:
MSH|^~\&|TestSendingSystem||||20090923152839||ADT^A01^ADT_A01|1956|P^T|2.4|123
PID|||123456||Doe^John

The message timestamp will vary as it reflects the point of time when the message is being created.

Create a ORU_R01 v2.5 message

The second example is to create a not-so-simple message, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/PopulateOBXSegment.html. The problem about this message is its nested structure of repeatable elements.
This time, the implementation also takes advantage of the HL7 v2 DSL, replacing all named field accessors by their position-based syntax.

In addition to the first example, this example shows

  • how to wrap a HAPI message into a HL7 v2 DSL MessageAdapter object
  • how to apply the HL7 v2 DSL
  • how to transparently add repetition to repeatable structures and fields
  • how to omit default indices due to the DSL's Smart Navigation feature
  • how to create composite and primitive fields from scratch
  • how to work with "Varies" types like in OBX-5
PopulateOBXSegment.groovy

import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension
import org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension
import org.openehealth.ipf.modules.hl7dsl.MessageAdapter

import ca.uhn.hl7v2.model.*

public class PopulateOBXSegment{
    
    static def makeOBX() {
        // Create message and wrap
        def msg = new MessageAdapter(Message.ORU_R01('2.5'))
        
        // Populate OBR. Group repetitions are created while navigating
        def obr = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBR
        obr[1] = '1'
        obr[3][1] = '1234'
        obr[3][2] = 'LAB'
        obr[4] = '88304' // Smart Navigation expands this to obr[4][1]

        // Populate the first OBX
        // Note that we don't specify the repetition with PATIENT_RESULT and
        // ORDER_OBSERVATION because Smart Navigation
        def obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(0).OBX
        obx[1] = '1'
        obx[2] = 'CE' // the type of OBX-5
        obx[3] = '88304'
        obx[4] = '1'
        def ce = Composite.CE(msg.target, 
           [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM'])
        // OBX-5 is a repeatable Varies field. 
        // Don't care about it, the DSL gets it right for you.
        obx[5] = ce  // equivalent with obx[5](0).data = ce
        
        // Populate the second OBX
        obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(1).OBX
        obx[1] = '2'
        obx[2] = 'TX' // the type of OBX-5
        obx[3] = '88304'
        // The second OBX in the sample message has an extra subcomponent at
        // OBX-3-1. This component is actually an ST, but the HL7 specification allows
        // extra subcomponents to be tacked on to the end of a component. This is
        // uncommon, but HAPI nontheless allows it.
        obx[3][1].extraComponents[0].data = Primitive.ST(msg.target, 'MDT')
        obx[4] = '2'
        def tx = Primitive.TX(msg.target, 
           'MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE')
        obx[5] = tx
        
        // Return the message
        msg
    }
    
    public static void main(def args){
        // This example requires both extension packages
        ExpandoMetaClass.enableGlobally()
        new HapiModelExtension().extensions.call()
        new Hl7ModelExtension().extensions.call()
        println makeOBX()
    }
}

The console output is:

MSH|^~\&|||||20090923151109||ORU^R01^ORU_R01|98|P^T|2.5
OBR|1||1234^LAB|88304
OBX|1|CE|88304|1|T57000^GALLBLADDER^SNM
OBX|2|TX|88304&MDT|2|MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE

Conclusion

The example showed that with IPF's HL7 support you can work with HL7 messages without ever seeing and touching much of the programming interface of the underlying HAPI library. The code is readable for HL7 v2 domain experts without much knowledge in programming.
This impressively underlines the purpose of domain specific languages to narrow the gap between domain experts and software development experts, here applied to the domain of HL7 v2 messaging.

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

HL7 v2 Message Validation

This chapter describes how IPF supports the validation of HL7 v2 messages.


Overview

HL7 v2.x is a complex flat-file structure that, despite being considered a data standard, is also highly flexible. It is often expected of an HL7 integration engine that non-standard compliant data be accepted and processed without notification to the receiving system of non-compliance.
However, to achieve real interoperability, the HL7 standard should be constrained to reduce the degree of freedom e.g. how to use certain fields or whether to populate optional fields or not. This happens either based on a written specification or in addition as machine-readble conformance profile. In order to check whether HL7 messages actually conform to the defined constraints, message validation is essential.

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

Validation rules are defined by instantiating an implementation of the ca.uhn.hl7v2.validation.ValidationContext interface of HAPI. Then you use this context to validate messages against the contained constraints.

Validation

There are two ways to validate:

  • while parsing a message
  • as dedicated operation on an existing message

To validate together with parsing, simply configure your Parser instance with the ValidationContext. If validation fails an exception is thrown.

   ...
   Parser parser = new PipeParser(context)
   Message message = parser.parse(msgText)
   // Does not reach code below on parsing or validation error
   ...

To validate after parsing, use the HAPI MessageValidator class. If validation fails an exception is thrown.

   import ca.uhn.hl7v2.validation.MessageValidator
   ...
   new MessageValidator(context, true).validate(message)
   // Does not reach code below on parsing or validation error
   ...

IPF HL7 Validation Rules

IPF provides a special ValidationContext: the org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext class. It offers two benefits:

  1. It provides access to a Validation Builder that allows for definition of validation rules using a simple Domain Specific Language (DSL)
  2. It supports Validation rules that constrain their target object by evaluating Groovy closures for better flexibility.

Although it is possible to manually instantiate the Validation rule classes that come with IPF, you normally should not care and instead use the Validation builder.
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.

import org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext
...
DefaultValidationContext context = new DefaultValidationContext()
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
       ...

Now the rules can be defined by specifying constraints on

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

Reusing a DefaultValidationContext

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()
       ...

It is also possible to reuse any ValidationContext implementation (i.e. also not IPF-based contexts) 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)
       ...

Configuring HL7 v2 Validation

This section explains how to configure Validation Context and Validation Rules in an application based on the Spring Framework.

  1. To initialize a DefaultValidationContext from a Spring Framework ApplicationContext, you need a ValidationContextFactoryBean and one or more ValidationContextBuilder beans. Each ValidationContextBuilder will contribute to the overall set of rules being applied.
    context.xml
    <beans>
    ...
       <!-- The FactoryBean creates an instance of DefaultValidationContext -->
       <bean id="context" 
           class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/>
    
       <!-- Rule Set 1: Default rules for primitive types that come with IPF -->
       <bean id="defaultTypeRules" 
           class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/>
    
       <!-- Rule Set 2: Custom rules of either variant (type, message or encoding) -->
       <bean id="myCustomRules" 
           class="com.my.company.MyValidationContextBuilder"/>
    
       <!-- Optional: initialize a Parser instance with the ValidationContext -->
       <bean id="parser" class="org.openehealth.ipf.modules.hl7.parser.PipeParser">
          <property name="validationContext" ref="context"/>
       </bean>	   
    ...
    </beans>
    
  2. Provide the custom ValidationContextBuilder implementation. A skeleton for such a custom builder is given in the following example:
    MyValidationContextBuilder.groovy
    package com.my.company
    
    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(...)
                   ...
       }
    }
    

Primitive Type Constraints

This section shows how to define constraints on primitive HL7 data types.

Standard validation rules provided

IPF already provides a set of constraints for primitive types as defined in the HL7 specification for each 2.x version. If this is fits your needs, use org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext as your ValidationContext implementation and optionally add more validation rules to it.

Primitive types have no substructure, i.e. they directly contain string values. The values are usually constrained by one or more of the following restrictions:

  • 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, however, regular expressions are sometimes hard to read and understand.

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 is mandatory and the month and the day is optional. The example uses a regular expression to specify the date pattern. Please also read about regular expressions in Groovy (http://groovy.codehaus.org/Regular+Expressions).

The Validation DSL for primitive types supports the following constraints:

Constraint type Method .type('X') Example constraints...
maximum length .maxSize(int) maxSize(255) ... the maximum length to 255 characters
length range [min..max] [10..20] ... the length to be between 10 and 20
existence .notEmpty()   ... that there must be a value of >= 1 character
matches .matches(regexp) matches(/(\d{4}) ... the value has four digits
number type .isNumber()   ... the value to be a decimal number
user defined .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, however, it's not recommended because it unnecessarily couples validation with preprocessing.

Constraint type Method .type('X') Example function
trim .omitLeadingWhitespace()   removes leading whitespace characters
trim .omitTrailingWhitespace()   removes trailing whitespace characters

You can combine any of constraint methods to define more than one rule for a type:

...
.type('X').omitLeadingWhitespace().isNumber().checkIf { it > 50 && it < 100 }
...

Each HL7 v2.x version defines a set of primitive types; over the time the number of types have increased and/or the constraints have been modified. IPF comes with predefined validation rules that enforce these constraints. Also note that the IPF PipeParser class is preconfigured with the default type rules.

import ca.uhn.hl7v2.validation.ValidationContext
import org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext
...
ValidationContext context = DefaultTypeRulesValidationContext()
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.

Message Constraints

This section explains how complete HL7 messages can be validated.

By default, the HAPI parsers accept about any message as long as it follows the HL7 syntactic rules. For messages, groups and segments not defined in the respective HL7 2.x standard, generic structures are internally instantiated. This allows parsing custom messages even without prior definition of Z segments or similar custom structures.
Because the parser itself can not guarantee specification-compliant messages, this has to be added either by configuring the parser or executing validation seperately.

When message validation rules are checked

Message rules can be enforced by either configuring the parser or by manually validating a parsed message.
Message rules are not enforced each time when a part of the message is modified.

There are three subtypes of message validation rules provided by IPF.

Validation Rule ValidationBuilder clause Description
HL7 Abstract Message Syntax abstractSyntax validates the existance, order, and cardinality of HL7 groups and segments
Conformance Profiles conformsToProfile validates the message against the static part of a HL7 Conformance Profile
Custom checkIf passes the Message object into as parameter into a Groovy closure, where custom validation can be executed

HL7 Abstract Message Syntax

HL7 v2 defines an Abstract Message Syntax (see HL7v2.5 specification, Chapter 2.13), that determines how 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 with the abstractSyntax builder expression. The corresponding rule is almost a copy of the definition, with only a few differences:

  • segment names are enclosed in quotes ('')
  • group names are specified like function calls inside the cardinality indicators as 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  ]

   context.configure()
     .forVersion('2.5')
        .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 fields inside segments can neither be specified nor validated with the Abstract Message Syntax.

HL7 Conformance Profiles

Conformance Profiles have been introduced with HL7 v2.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 HL7 v2.5 specification document.
There are tools that facilitate the definition of Conformance Profiles, most notably the Messaging Workbench, which can be downloaded at http://sl.infoway-inforoute.ca/downloads/MWB%20Release%206-7p1.zip.

HAPI supports checks against conformance profiles. In the IPF Validation framework it can be used with the conformsToProfile builder expression.

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.

Custom Message 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
         }

Encoded Message Constraints

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()
...

Camel DSL Extensions for HL7 v2

This chapter shows how HL7 Messaging support can be used in IPF integration routes.


Configuring Camel DSL Extensions for HL7 v2 Messaging

This section explains how to configure the DSL extensions for HL7 messaging support.

Make sure you already have correctly set up

  • Maven 2.0.9 or better
  • a possibly empty IPF project

After configuration you will be able to use the HL7 v2 related DSLs together with Apache Camel's routing engine.

  1. Add the necessary dependencies to your project's Maven 2 descriptor.
    pom.xml for integration scenario
    ...
    <!-- Dependency for DSL extensions for HL7 v2 -->
    <dependency>
        <groupId>org.openehealth.ipf.platform-camel</groupId>
        <artifactId>platform-camel-hl7</artifactId>
        <version>${ipf-version}</version>
    </dependency>
    
    ...
    


  2. Depending on the HL7 v2 versions, add the corresponding dependencies to the HAPI library. HAPI supports versions 2.2 through 2.6.
    pom.xml
    ...
    <!-- Dependency for HL7 v2.5 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v25</artifactId>
        <version>0.6</version>
    </dependency>
    
    <!-- Dependency for HL7 v2.5.1 -->
    <dependency>
        <groupId>ca.uhn.hapi</groupId>
        <artifactId>hapi-structures-v251</artifactId>
        <version>0.6</version>
    </dependency>
    ...
    


  3. Register the Camel extensions in the Spring Application Context of your application. If you use the functional extensions to HAPI, register the HapiModelExtensions as well.
    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://camel.apache.org/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://camel.apache.org/schema/spring
    http://camel.apache.org/schema/spring/camel-spring.xsd
    
        <!-- Setting up the Camel context -->
    
        <camel:camelContext id="camelContext">
            <camel:routeBuilder ref="routeBuilder" />
        </camel:camelContext>
        
        <bean id="routeBuilder" depends-on="routeModelExtender" class="..." />
    
        ...
    
        <!-- HAPI extensions -->
        <bean id="hapiModelExtension" 
            class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
            <property name="mappingService" ref="..." />
        </bean>
    
        <!-- General Mapping extensions -->
        <bean id="mappingExtension" 
            class="org.openehealth.ipf.commons.map.extend.MappingExtension">
            <property name="mappingService" ref="..." />
        </bean>
    
        <!-- Camel DSL extensions -->
        <bean id="hl7ModelExtension" 
            class="org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension">
        </bean>
    
        <!-- Register the extensions -->
        <bean id="routeModelExtender"
            class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
            <property name="routeModelExtensions">
                <list>
                    ...
                    <ref bean="hl7ModelExtension" />
                    <ref bean="hapiModelExtension" />
                    <ref bean="mappingExtension" />
                </list>
            </property>
        </bean>
    
        ...
    
    </beans>
    

DSL extensions for HL7 v2 Messaging

This section describes the DSL extensions for using HL7 v2 together with Apache Camel, provided by the platform-camel-hl7 component. For a description of Camel-independent HL7 message processing features visit HL7 Messaging and HL7 Message Validation.

The main purpose of this set of DSL extensions is to make the HL7 v2 DSL, HAPI extensions and HL7 v2 Validation features available in Camel routes. Extensions provided may well be combined with other extensions that comply with the DSL extension mechanism.

HL7 v2 DSL MessageAdapter (un)marshalling

The ghl7() DSL extension allows you to convert between HL7 message strings (or streams) and org.openehealth.ipf.modules.hl7dsl.MessageAdapter objects. For example, to unmarshal a message adapter from a string (or stream) use

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

To marshal a message adapter to an output stream use

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

(Un)marshaling options

HL7 v2 DSL MessageAdapter unmarshalling and marshalling can be customized in various ways. You can define

  • a custom character set via the ghl7(java.lang.String charset) parameter,
  • a custom HAPI parser via the ghl7(ca.uhn.hl7v2.parser.Parser parser) parameter
  • or both via ghl7(ca.uhn.hl7v2.parser.Parser parser, java.lang.String charset)

The charset parameter is used to define the character set used for reading from and writing to a byte stream.
The parser parameter allows you to define a custom HAPI parser when you unmarshal a message adapter from a stream. The message adapter adapts a HAPI message which in turn is created by a HAPI parser.

Here's an example:

(Un)marshaling options
    def ca.uhn.hl7v2.parser.Parser parser = new MyCustomParser()

    // ...
    .unmarshal().ghl7(parser, 'ISO-8859-1')
    // ...
    .marshal().ghl7('ISO-8859-1')
    // 

HL7 v2 Message Validation

HL7 v2 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 v2 Message Validation. Here's an example:

Message validation with static profiles
    // Create or configure a custom HL7 validation context.
    DefaultValidationContext 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 DSL Extensions for HL7 v2 Message Validation 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 ValidationContext 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().

Camel DSL Extensions for HL7 v2 Messaging Example

Often, for processing HL7 messages you might first want to unmarshal a MessageAdapter from an InputStream, validate the message using either a default or custom validation context and then use the HL7 DSL in processors, transmogrifiers or content based routers. Then the processing result is marshalled again for being transported to another endpoint.

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 the
    // routing decisions are made based in the MSH[5] field value of the HL7 message 
    // (using the HL7 v2 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')

IHE support

Early access

Please note that the following documentation and the underlying code for IHE support is early access and might be subject to change. We also didn't test IHE support in OSGi environments yet.

Concepts

The IPF provides support for several IHE profiles. The basic idea is to offer a Camel component for each IHE transaction. These components ensure that the technical requirements of the profile are met by applications built on top of the IPF IHE support.

Quick reference

All IPF IHE components are named according to the profile and transaction that they implement. Each profile corresponds to a Maven artifact in the group org.openehealth.ipf.platform-camel, whereby each artifact contains a set of components, as listed below:

Artifact platform-camel-xds

IHE Transaction ID Description IPF IHE component
ITI-14 XDS.a Register Document Set xds-iti14
ITI-15 XDS.a Provide & Register Document Set xds-iti15
ITI-16 XDS.a Query Registry xds-iti16
ITI-17 XDS.a Retrieve Document xds-iti17
ITI-18 XDS.a+b Registry Stored Query xds-iti18
ITI-41 XDS.b Provide & Register Document Set xds-iti41
ITI-42 XDS.b Register Document Set xds-iti42
ITI-43 XDS.b Retrieve Document Set xds-iti43

Artifact platform-camel-pixpdq

IHE Transaction ID Description IPF IHE component
ITI-8 Patient Identity Feed (PIX Feed) pix-iti8
xds-iti8
ITI-9 PIX Query pix-iti9
ITI-10 PIX Update Notification pix-iti10
ITI-21 Patient Demographics Query (PDQ) pdq-iti21
ITI-22 Patient Demographics and Visit Query (PDQ) pdq-iti22

Artifact platform-camel-pixpdqv3 (IPF 2.1 only)

IHE Transaction ID Description IPF IHE component
ITI-44 Patient Identity Feed (PIX Feed) v3 pixv3-iti44
xds-iti44
ITI-45 PIX Query v3 pixv3-iti45
ITI-46 PIX Update Notification v3 pixv3-iti46
ITI-47 Patient Demographics Query (PDQ) v3 pdqv3-iti47

Moreover, there are packages which serve for translation of request and response messages of selected ITI transactions between HL7 v3 and HL7 v2:

XDS.b

The IHE XDS.b profile mandates the use of the web service technology. The IPF provides these web services in conformance with the specification, together with their WSDL documents. The XDS.b actors (Registry, Repository, Source and Consumer) are not directly implemented by the IPF. Instead, the transactions required by the actors are offered via components that create endpoints for both the client and server sides. An application assembles these components to build the actors:

Note that registries are also required to support the PIX feed transaction, which is covered in the corresponding section.

XDS standard conformance

The IPF implementation of the XDS profiles conforms to Version 6.0 (August 2009) of the IHE infrastructure technical framework with the addition of the final texts of the change proposals up to CP 426.

Because the XDS components do not represent a full implementation of the XDS actors, they cannot fulfill all parts of the specification. E.g. they cannot check whether a patient ID is known to the registry. Such checks need to be performed within the route implementation. The following list should give a brief overview of the parts of the requirements that have been met by the IPF implementation:

  • protocol specific requirements: most notably those listed in Appendix V of the ITI Technical Framework Volume 2x (Web Services for IHE Transactions). This includes WS-Addressing, SOAP 1.2/1.1 and mustUnderstand attributes in SOAP headers.
  • meta class model requirements: as listed in chapter 4 of the ITI Technical Framework Volume 3 (Cross-Transaction Specifications). This also includes transformations between ebXML classes and meta model classes where applicable.
  • stored query classes: as an extension to the meta model, simplified classes have been defined for each stored query type specified in section 3.18 of the ITI Technical Framework Volume 2a (Registry Stored Query).

XDS.b configuration

To use of the IPF XDS.b components in an application, you have to add the dependencies of the profile component to your pom.xml:

<dependency>
    <groupId>org.openehealth.ipf.platform-camel</groupId>
    <artifactId>platform-camel-ihe-xds</artifactId>
    <version>${ipf-version}</version>
</dependency>

The web service implementation within the IPF component is based on Apache CXF. The necessary dependencies are transitively included via the above dependency. CXF itself can be used in various ways. For the purpose of this documentation we will use Spring configuration via imports and deploy the components into Apache Tomcat. The following snippet is an example for an application context that imports all relevant CXF and IPF beans:

context.xml
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:camel="http://camel.apache.org/schema/spring"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd
">

    <!-- Importing the CXF configuration -->
    <import resource="classpath:META-INF/cxf/cxf.xml"/>
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
    <import resource="classpath:META-INF/cxf/cxf-extension-addr.xml"/>

    <camel:camelContext id="camelContext">
        <camel:jmxAgent id="agent" disabled="true"/>
        <camel:routeBuilder ref="routeBuilder"/>
    </camel:camelContext>

    <bean id="routeBuilder" depends-on="routeModelExtender"
        class="org.openehealth.ipf.platform.camel.ihe.xds.iti18.GroovyRouteBuilder">
    </bean>

    <bean id="coreModelExtension"
        class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension">
    </bean>

    <bean id="xdsModelExtension"
        class="org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension">
    </bean>

    <bean id="routeModelExtender"
        class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
        <property name="routeModelExtensions">
            <list>
                <ref bean="coreModelExtension" />
                <ref bean="xdsModelExtension" />
            </list>
        </property>
    </bean>
</beans>

Deployment within Tomcat is done by defining a web.xml that references the above application context as well as instantiating a CXF-related servlet. A simple setup could look like this:

web.xml
<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>Test IPF IHE Web-App</display-name>
  <context-param>
    <!-- configures the classpath of the Spring application context -->
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:path/to/your/context.xml</param-value>
  </context-param>

  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>
  <servlet>
    <servlet-name>CXFServlet</servlet-name>
    <servlet-class>
        org.apache.cxf.transport.servlet.CXFServlet
    </servlet-class>
  </servlet>
  <servlet-mapping>
    <!-- configures the address of the servlet path under which our web services are published -->
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

Exposing an XDS.b service

Similar to other Camel components, the XDS.b components are used within your routes:

from('xds-iti18:myIti18Service')
   ...

This statement creates a web service that implements the ITI-18 transaction and publishes it under the service name myIti18Service. The full path of the service URL is made up by the web container and the servlet configuration. With the above configuration, the service will be published under http://HOSTNAME:PORT/WEBAPP/services/myIti18Service, where HOSTNAME is the name of the machine that the service is running on, PORT represents the TPC/IP port configured within the web container (e.g. in Tomcat you can configure this in server.xml, default value is 8080), and WEBAPP is a name that depends on your web application deployment (in Tomcat this is simply the name of the directory of the web application). More information about how to setup a simple web application with Apache Tomcat can be found here.

Secure transport can be used with the XDS.b services by configuring your container. There is no additional configuration required for IPF XDS components. Details on how to configure Apache Tomcat with SSL support can be found here.

Making calls to an XDS.b service

You access an XDS.b service within a route similar to other Camel endpoints:

   ...
   .to('xds-iti18://localhost:8080/myWebApp/services/myIti18Service')

The whole endpoint URI resembles the service URL as it was described in the previous section. The only difference is that the protocol name is changed from http to xds-iti18.

Per default, all requests will be sent using SOAP 1.2, as prescribed by the specification. To switch to SOAP 1.1, URL parameter soap11=true can be used. Note that because of a bug in CXF, XDS.b consumer components will produce SOAP 1.2 responses independently from the actual SOAP version used in requests. This will be corrected as soon as the new CXF version will be available for the IPF. (Note 2010-02-10: parameter "soap11" is not supported any more)

Secure transport via HTTPS can be configured by adding the URL parameter secure=true. The following sample shows a call to a registry over HTTPS:

   ...
   .to('xds-iti18://localhost:8080/myWebApp/services/myIti18Service?secure=true')

In addition, the HTTP client used by CXF must be configured as well. This is done within the application context according to the CXF documentation:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:camel="http://camel.apache.org/schema/spring"
    xmlns:http="http://cxf.apache.org/transports/http/configuration"
    xmlns:sec="http://cxf.apache.org/configuration/security"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd
http://cxf.apache.org/transports/http/configuration
http://cxf.apache.org/schemas/configuration/http-conf.xsd
http://cxf.apache.org/configuration/security
http://cxf.apache.org/schemas/configuration/security.xsd
">

    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <http:conduit name="*.http-conduit">
        <http:tlsClientParameters disableCNCheck="true">
            <sec:keyManagers keyPassword="changeit">
                <sec:keyStore type="JKS" password="changeit" file="keystore" />
            </sec:keyManagers>
            <sec:trustManagers>
                <sec:keyStore type="JKS" password="changeit" file="keystore" />
            </sec:trustManagers>
            <sec:cipherSuitesFilter>
                <!--
                    these filters ensure that a ciphersuite with export-suitable or
                    null encryption is used, but exclude anonymous Diffie-Hellman
                    key change as this is vulnerable to man-in-the-middle attacks
                -->
                <sec:include>.*_EXPORT_.*</sec:include>
                <sec:include>.*_EXPORT1024_.*</sec:include>
                <sec:include>.*_WITH_DES_.*</sec:include>
                <sec:include>.*_WITH_NULL_.*</sec:include>
                <sec:exclude>.*_DH_anon_.*</sec:exclude>
            </sec:cipherSuitesFilter>
        </http:tlsClientParameters>
    </http:conduit>
    ...

Message types

Messages sent between XDS.b actors use a payload defined by ebXML 3.0. The profile specifies how messages are created to represent requests and responses as well as document entries, submission sets and folders. Besides the support of raw ebXML 3.0 messages, the IPF offers a model to abstract from the underlying details of transforming and validating ebXML 3.0 messages.

Raw ebXML 3.0 support

The body of input and output messages depends on the transaction:

Transaction Input message body Output message body
ITI-18 org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryRequest org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryResponse
ITI-41 org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.ProvideAndRegisterDocumentSetRequestType org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.rs.RegistryResponseType
ITI-42 org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.lcm.SubmitObjectsRequest org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.rs.RegistryResponseType
ITI-43 org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.RetrieveDocumentSetRequestType org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.RetrieveDocumentSetResponseType

The IPF also provides the necessary ebRS 3.0 classes that are referenced within these types. All ebRS 3.0 related classes can be found in the sub-packages of org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.

Simplified model classes

ebXML 3.0 message bodies can be converted to a simplified model. The following table shows the types that can be used for each transaction:

Transaction Input message body Output message body
ITI-18 org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse
ITI-41 org.openehealth.ipf.commons.ihe.xds.core.requests.ProvideAndRegisterDocumentSet org.openehealth.ipf.commons.ihe.xds.core.responses.Response
ITI-42 org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet org.openehealth.ipf.commons.ihe.xds.core.responses.Response
ITI-43 org.openehealth.ipf.commons.ihe.xds.core.requests.RetrieveDocumentSet org.openehealth.ipf.commons.ihe.xds.core.responses.RetrievedDocumentSet

These model classes can be used via type converters, as shown in the following Groovy route:

import static org.openehealth.ipf.commons.ihe.xds.core.responses.Status.*

import org.apache.camel.spring.SpringRouteBuilder
import org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry
import org.openehealth.ipf.commons.ihe.xds.core.requests.query.FindDocumentsQuery
import org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse
import org.openehealth.ipf.commons.ihe.xds.core.metadata.ObjectReference
import org.openehealth.ipf.platform.camel.core.util.Exchanges

public class RouteBuilder extends SpringRouteBuilder {
    @Override
    public void configure() throws Exception {
        from('xds-iti18:myIti18Service')
            .convertBodyTo(QueryRegistry.class)
            .choice()
                // Return an object reference for a find documents query
                .when { it.in.body.query instanceof FindDocumentsQuery }
                    .transform {
                        def response = new QueryResponse(SUCCESS)
                        response.references.add(new ObjectReference('document01'))
                        response
                    }
                // Any other query else is a failure
                .otherwise()
                    .transform { new QueryResponse(FAILURE) }
    }
}

This route only accepts the stored query FindDocuments. If it receives such a query, it returns a single reference to a document (document01). Any other query returns a failure response. Of course this route does not fulfill the functional requirements of the ITI-18 transaction (e.g. all stored query types must be supported by a registry). However, it shows how a registry can be implemented without directly using the ebXML 3.0 classes.

convertBodyTo

Note that convertBodyTo(QueryRegistry.class) is not strictly required. If it is not used, the conversion must be done when accessing the body. In this case the when statement needs to use it.in.getBody(QueryRegistry.class).query. This will leave the body unchanged. However, keep in mind that converting the ebXML object every time you need to access the body can be time consuming.

Large document content

The transactions ITI-41 and ITI-43 have to send document content as part of their request or response messages. In practice such messages can become quite large. To allow for memory-efficient streaming of the document content, the aforementioned components rely on CXF's support for binary data. CXF streams the content on disk and then provides a DataHandler to access the file. Therefore, it is not necessary to use IPF's large binary support (LBS).

Validation

The XDS.b components provide functionality for transaction-specific validation of ebXML 3.0 messages. This validation uses the validate DSL extension of the IPF. The following code snippet shows an example of a validation in

from('xds-iti18:myIti18Service')
   .validate().iti18Request()
   ... // Process the message and create a response.
   .validate().iti18Response()

The corresponding DSL extensions for validation need to be included into the model extender. Make sure that the following beans are defined within your application context:

...
<bean id="coreModelExtension"
      class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension" />

<bean id="xdsModelExtension"
      class="org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension" />

<bean id="routeModelExtender"
      class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
    <property name="routeModelExtensions">
        <list>
            <ref bean="coreModelExtension" />
            <ref bean="xdsModelExtension" />
            <!-- ... other model extension bean if necessary ... -->
        </list>
    </property>
</bean>
...

XDS.a

Support for XDS.a is similar to that of XDS.b. Most transactions presume web service-based endpoints; only ITI-17 directly uses HTTP for the download of documents. With the exception of ITI-18, which is the same in both XDS.a and XDS.b, XDS.a transaction numbers differ from those in XDS.b and use SOAP 1.1 and SwA (SOAP with Attachments) instead of SOAP 1.2 and MTOM respectively. The following image gives a quick overview of the actors and transactions involved in the XDS.a profile:

Note that the Registry actor has to support the PIX feed transaction, which is covered in the corresponding section.

Configuring, exposing and accessing of the XDS.a components are to be performed in the same way as in the case of XDS.b. As already mentioned above, the only significant difference is that the ITI-17 transaction is not implemented as a SOAP-based web service. Therefore, it requires a few additional steps that are mentioned in the next sections.

XDS standard conformance

The IPF implementation of the XDS profiles conforms to Version 6.0 (August 2009) of the IHE infrastructure technical framework with the addition of the final texts of the change proposals up to CP 426.

ITI-17 configuration

To offer the ITI-17 transaction for a repository implementation you have to add an additional servlet to your web.xml:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>Test IPF IHE Web-App</display-name>
  <context-param>
    <!-- configures the classpath of the Spring application context -->
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:org/openehealth/ipf/platform/camel/ihe/xdsb/example/context.xml</param-value>
  </context-param>

  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

  <servlet>
    <!-- Servlet used for all CXF web services -->
    <servlet-name>CXFServlet</servlet-name>
    <servlet-class>
        org.apache.cxf.transport.servlet.CXFServlet
    </servlet-class>
  </servlet>

  <servlet>
    <!-- Servlet used only for ITI-17 -->
    <servlet-name>Iti17Servlet</servlet-name>
    <servlet-class>
        org.openehealth.ipf.platform.camel.ihe.xds.iti17.servlet.Iti17Servlet
    </servlet-class>
  </servlet>

  <servlet-mapping>
    <!-- configures the address of the servlet path under which our web services are published -->
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <!-- configures the address of the servlet path under which the ITI-17 transaction is published -->
    <servlet-name>Iti17Servlet</servlet-name>
    <url-pattern>/iti17/*</url-pattern>
  </servlet-mapping>
</web-app>

Exposing the ITI-17 transaction

The ITI-17 transaction is exposed via

from('xds-iti17:myIti17Service')
    ...

The URL that this transaction is accessible under is determined by the ITI-17 servlet configuration in your web.xml. For the above sample web.xml, the URL results in http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service. Note that the servlet path (iti17) is different to that of other transactions (services) that use the CXFServlet.

Making calls to the ITI-17 transaction

Calling the ITI-17 transaction from a document consumer can be achieved via:

   ...
   .to('xds-iti17://localhost:8080/myWebApp/iti17/myIti17Service')

Again, note the difference in the URL. The ITI-17 servlet is accessed through a different path than the CXFServlet.

Message types

Messages sent between XDS.a actors use ebXML 2.1 (ebXML 3.0 for ITI-18) as payload format. The profile specifies how messages are created to represent requests and responses as well as document entries, submission sets and folders. Besides the support for raw ebXML messages, the IPF offers a model to abstract from the underlying details of transforming and validating ebXML messages.

Raw ebXML 2.1/3.0 support

The body of input and output messages depend on the transaction:

Transaction Input message body Output message body
ITI-14 org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.SubmitObjectsRequest org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse
ITI-15 org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml21.ProvideAndRegisterDocumentSetRequestType org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse
ITI-16 org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.query.AdhocQueryRequest org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse
ITI-17 String (document URL part) InputStream
ITI-18 org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryRequest org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryResponse

The IPF also provides the necessary ebRS 2.1 and 3.0 classes that are referenced within these types. All ebRS-related classes can be found in the sub-packages of org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30 and org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.

The input of the ITI-17 transaction is the part of the document URL that is appended to the URL of the service. The exact structure depends on the implementation of the Repository actor. It can be a path, a query, or a combination of both:

Document URL ITI-17 input message body
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service/my/path/to/document
/my/path/to/document
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service?docId=123
?docId=123
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service/my/path?docId=321
/my/path?docId=321

Simplified model classes

ebXML message bodies can be converted to a simplified model. The following table shows the types that can be used for each transaction:

Transaction Message input body type Message output body type
ITI-14 org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet org.openehealth.ipf.commons.ihe.xds.core.responses.Response
ITI-15 org.openehealth.ipf.commons.ihe.xds.core.requests.ProvideAndRegisterDocumentSet org.openehealth.ipf.commons.ihe.xds.core.responses.Response
ITI-16 org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse
ITI-18 org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse

For an example of how to use these classes, refer to the corresponding XDS.b section.

Large document content

XDS.a uses SOAP with Attachments (SwA) instead of MTOM, which is used in XDS.b. The implementation of SwA within CXF has to be checked for efficient support for large documents. This is currently an open issue. However, it only applies to the ITI-15 transaction that performs the "upload" of documents. The ITI-17 transaction is a simple HTTP GET download that results in an InputStream contained in the exchange. This allows efficient streaming of the document.

Validation

Apart from ITI-17, XDS.a messages are validated similar to XDS.b messages. For ITI-17 there is no validation support.

PIX + PDQ

The IPF provides components for PIX and PDQ transactions that use HL7 v.2 as data format and MLLP as transport protocol. The following picture shows the relevant actors and transactions:

In fact, these components represent extensions of the standard camel-mina component. Exactly as for XDS.a+b, the application's pom.xml must state the corresponding dependency:

<dependency>
    <groupId>org.openehealth.ipf.platform-camel</groupId>
    <artifactId>platform-camel-ihe-pixpdq</artifactId>
    <version>${ipf-version}</version>
</dependency>

This will automatically (transitively) involve, in particular, camel-mina, camel-hl7, HAPI and IPF HL7 processing support.

Starting from version 2.1-m2, the IPF provides client-side and server-side support for HL7 interactive message continuation, unsolicited message fragmentation as well as segment fragmentation.

Making and accepting calls via PIX/PDQ endpoints

The URL format is the same for consumers and producers:

from('pix-iti8://0.0.0.0:8888?param1=value1&param2=value2')
   ...
   ...
   .to('pix-iti8://hostname.org:9090?param3=value3&param4=value4')

Only parameters are optional, all other URL parts are obligatory.

URL Parameters

There are two groups of URL parameters in the PIX/PDQ components: the ones inherited from the basis Camel components and the additional ones.

Parameters inherited from camel-mina and camel-hl7

Some parameters defined in camel-mina have obtained constant values in the PIX/PDQ components. It means that these parameters are actually not configurable by the user any more; values provided via endpoint URLs are silently ignored. These parameters are the following:

Parameter name Type Constant value in PIX/PDQ components
sync boolean true
lazySessionCreation boolean true
transferExchange boolean false
encoding String corresponds to the charset name configured for the HL7 codec factory, as described below

Moreover, camel-hl7 defines a parameter named codec, which contains the name of a Spring bean that corresponds to an HL7 codec factory. PIX/PDQ components set "#hl7codec" as a default value for this parameter. The user still has to define the corresponding bean, though:

<bean id="hl7codec" class="org.apache.camel.component.hl7.HL7MLLPCodec">
    <property name="charset" value="iso-8859-1"/>
</bean>

The character set name set up for the HL7 codec factory will be automatically

  • propagated to the Camel component (see parameter encoding in the table above),
  • stored in the Exchange.CHARSET_NAME property of each Camel exchange, and
  • used in all data transformation activities.

SSL-related URL parameters

Security aspects of MLLP-based transactions are controlled by the following URL parameters:

Parameter name Type Default value Short description
secure boolean false whether SSL should be supported by the given endpoint
mutualTLS boolean false whether client authentication for mutual TLS is required on the given endpoint
sslContext String - Spring bean name of a user-defined SSL context, if any, optionally with leading '#'. If not set, a default SSL contents will be used
sslProtocols String system defaults comma-separated list of SSL protocols which should be supported by the given endpoint
sslCiphers String system defaults comma-separated list of SSL cipher suites which should be supported by the given endpoint

URL parameters for HL7v2 continuation and fragmentation

HL7v2 continuation and fragmentation features can be configured by means of the following URL parameters:

Parameter name Type Default value Short description
supportInteractiveContinuation boolean false whether interactive message continuation should be supported by the given endpoint
interactiveContinuationDefaultThreshold int -1 default threshold (maximal count of data records per message) for interactive message continuation. Relevant on consumer side only and will be used only if RCP-2-2 of the request does not equal to 'I' or RCP-2-1 does not contain a parseable value. Values smaller than 1 lead to no continuation
interactiveContinuationStorage String - Spring bean name of a storage for interactive continuation chains. If not set, a default in-memory storage will be used
supportUnsolicitedFragmentation boolean false whether unsolicited message fragmentation should be supported by the given endpoint
unsolicitedFragmentationThreshold int -1 threshold (maximal count of segments per message) for unsolicited message fragmentation. Relevant on producer side only. Values smaller than 3 lead to no fragmentation.
supportSegmentFragmentation boolean false whether segment fragmentation should be supported by the given endpoint
segmentFragmentationThreshold int -1 threshold (maximal count of characters per segment) for segment fragmentation. Values smaller than 5 lead to no segment fragmentation.

Other URL parameters

Parameter name Type Default value Short description
audit boolean true whether ATNA audit should be performed on the corresponding endpoint
allowIncompleteAudit boolean false whether incomplete ATNA audit resords are allowed to be sent to the audot repository as well
interceptorBeans String - user-defined MLLP interceptors — a comma-separated list of Spring bean names representing instances of org.openehealth.ipf.platform.camel.ihe.mllp.core.intercept.MllpCustomInterceptor

Handling of data types and exceptions

Incoming messages (i.e. requests on the consumer side and responses on the producer side) are automatically unmarshalled into IPF MessageAdapter's.

Outgoing messages on the producer side (i.e. requests) are expected to belong to one of the following types in order to be able to be successfully marshalled and sent:

  • IPF MessageAdapter
  • "raw" HAPI message
  • String
  • byte[]
  • NIO ByteBuffer
  • InputStream
  • File
  • org.apache.camel.component.file.GenericFile<File>

For outgoing messages on the consumer side (i.e. responses), the same set of data types as for the producer side requests is supported. An addition, the message body can contain an Exception instance, which will be transformed into a NAK response. Any exceptions thrown in the route will lead to NAK responses as well.

When neither the data type of the response message is supported nor an exception has been thrown in the route, the message header org.openehealth.ipf.platform.camel.ihe.mllp.core.MllpComponent.ACK_TYPE_CODE_HEADER will be taken into consideration. When the value of this header belongs to the enumeration type org.openehealth.ipf.modules.hl7.AckTypeCode, an acknowledgement will be automatically generated and sent back to the requestor — a positive one for AckTypeCode.AA, a negative one (NAK) for AckTypeCode.AE and AckTypeCode.AR.

When even this header is not set or when its value is not of desired type, the route fails.

Validation

Same as for XDS.a+b messages, the IPF provides a validation mechanism for PIX/PDQ requests and responses. To use it, corresponding DSL extensions should be activated in the application's Spring descriptor as shown below:

...
<bean id="mllpModelExtension"
     class="org.openehealth.ipf.platform.camel.ihe.mllp.core.extend.MllpModelExtension"/>

<bean id="routeModelExtender"
      class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
    <property name="routeModelExtensions">
        <list>
            <ref bean="coreModelExtension" />
            <ref bean="mllpModelExtension" />
        </list>
    </property>
</bean>
...

After that, the validation can be performed using the validate DSL extension, e.g. for PIX Feed (ITI-8):

from('xds-iti8://0.0.0.0:9999?audit=false')
    .onException(ValidationException.class)
        .maximumRedeliveries(0)
        .end()
    .validate().iti8Request()
    .process {
        // generate a response
        Exchanges.resultMessage(it).body = ...
    }
    .validate().iti8Response()

Continuation and fragmentation support

Implemented features correspond to Sections 2.10.2.1 (Segment fragmentation/continuation using the ADD segment), 2.10.2.2 (Segment fragmentation/continuation using the DSC segment), and 5.6.3 (Interactive continuation of response messages) of the HL7 v2.5 specification, and are being referred to as "segment fragmentation", "unsolicited (message) fragmentation" and "interactive (message) continuation", respectively.

Segment fragmentation

Segment fragmentation is supported on both producer and consumer sides, for both incoming and outgoing messages. This support can be activated by setting the URL parameter supportSegmentFragmentation of the corresponding endpoint to true. For outgoing messages, the additional parameter segmentFragmentationThreshold should be set to an integer value greater on equal to 5 — it denotes the maximal allowed length of segments in outgoing messages, without consideration of segment separators '\r'. For example, when this threshold equals to 10, the message

MSH|^~\\&|MESA_PD_CONSUMER|MESA_DEPARTMENT|MESA_PD_SUPPLIER|PIM|20081031112704||QBP^Q22|324406609|P|2.5|||ER
QPD|IHE PDQ Query|1402274727|@PID.3.1^12345678~@PID.3.2.1^BLABLA~'@PID.3.4.2^1.2.3.4~@PID.3.4.3^KRYSO|||||
RCP|I|10^RD|||||

will be sent as

MSH|^~\&|M
ADD|ESA_PD
ADD|_CONSU
ADD|MER|ME
ADD|SA_DEP
ADD|ARTMEN
ADD|T|MESA
ADD|_PD_SU
ADD|PPLIER
ADD||PIM|2
ADD|008103
ADD|111270
ADD|4||QBP
ADD|^Q22|3
ADD|244066
ADD|09|P|2
ADD|.5|||E
ADD|R
QPD|IHE PD
ADD|Q Quer
ADD|y|1402
.....

Note that segment fragmentation across messages (described in Section 2.10.2.3 of the HL7 v2.5 specification) is not supported yet.

Unsolicited message fragmentation

This feature can be activated by setting the URL parameter of the corresponding endpoint supportUnsolicitedFragmentation to true.

On producer side, this will lead to automatical fragmentation of outgoing request messages based on the value of the URL parameter unsolicitedFragmentationThreshold, which denotes maximal count of segments allowed in an outgoing message. This value should be greater than 2 in order to "let in" at least one segment in addition to MSH and DSC. The producer will perform all necessary actions of the corresponding message exchange pattern to transport these fragments to the receiver, as depicted on the following interaction diagram (click to enlarge):

An IPF PIX/PDQ consumer with enabled segment fragmentation support is able to automatically collect pieces of fragmented requests and put them into the route as a single cumulative request message (click to enlarge):

Segment MSH of the resulting request message is the one of the very first fragment, therefore the corresponding response will contain its control ID (MSH-10) in its MSA-2.

On both producer and consumer sides, all unsolicited fragments or fragment requests (messages with filled MSH-14 and DSC-1, respectively), which cannot be attributed to any active conversation, will be unchanged passed through to the route. Unsolicited fragmentation is not possible, when the request message to be fragmented does already contain non-empty values in the fields MSH-14 and/or DSC-1.

Interactive continuation

Interactive continuation is like unsolicited fragmentation, but relates to response messages instead of request ones and uses data records count instead of segments count as the message splitting criterion (HL7 specification declares some other counts — e.g. those of lines, characters, or pages — to be usable as splitting criteria as well, but they are not supported by the IPF). What a "data record" does actually mean thereby, is transaction-dependent — for example, in PDQ (ITI-21), each data record corresponds to a QUERY_RESPONSE group which consists of segments PID, PD1, NK1, and QRI (data record definitions for ITI-21 and ITI-22 are available in the IPF out-of-the-box).

Support for interactive continuation can be enabled for an endpoint by setting its URL parameter supportInteractiveContinuation to true.

In this case, a consumer will apply transaction-specific rules to split the messages into fragments, using threshold value from the field RCP-2-1, provided that RCP-2-2 is equal to "RD". If the mentioned threshold field is not filled in the expected way, the value of the URL parameter interactiveContinuationDefaultThreshold will be used. When this parameter is not configured as well or its value is less than 1, no message splitting will be performed.

Interaction steps performed by the consumer are shown on the diagram below (click to enlarge):

Each fragment can be requested more than once, in arbitrary order. Fragments are stored internally, whereby the user has the possibility to provide a custom storage bean via the interactiveContinuationStorage URL parameter of the consumer endpoint. This bean must implement the interface org.openehealth.ipf.platform.camel.ihe.mllp.core.ContinuationStorage, refer to javadocs for details. Per default, an in-memory storage will be used.

Obsolete fragment chains can be removed from the storage either by means of a corresponding cancel message QCN^J01 sent by the client (as usual, such messages are automatically served by the IPF), by calling the storage's method purge() (for example, from a timer-based Camel endpoint), or by some proprietary mechanisms of the storage, if available.

When an IPF PIX/PDQ producer with enabled interactive continuation support recognizes, that the response message it just received is actually the first fragment of an interactive chain, it automatically adds its segment DSC to the initial request message and sends the latter again, as prescribed by the HL7 specification, requesting the next fragment in that way. This step will be repeated for all subsequent fragments until the last fragment of the chain has arrived. After that, the producer joins all collected data records together (using the same transaction-specific rules as the consumer used) and delivers the cumulative response to the caller (i.e. to the Camel route containing the corresponding .to(...) statement). The diagram below shows these interaction steps (click to enlarge):

Same as in case of unsolicited message fragmentation, all unexpected fragments will be passed through to the route without changes. This rule applies to cancel messages which relate to non-existent interactive chains (represented by their query tags) as well.

Combination of fragmentation/continuation features

Segment fragmentation can be harmlessly combined with both unsolicited message fragmentation and interactive message continuation. Note that in case of outgoing request messages, unsolicited message fragmentation is performed before segment fragmentation, therefore the resulting count of segments can be actually greater than the value of the unsolicitedFragmentationThreshold parameter.

On the other hand, simultaneous activation of unsolicited message fragmentation and interactive message continuation can be problematic, because each of them makes use of the same fields in the segment DSC. For example, when a request message was sent using unsolicited fragmentation, and the response represents an interactive continuation fragment, it will be impossible to send the request for the second response fragment using unsolicited fragmentation, because DSC-1 will contain a non-empty value.

PIXv3 + PDQv3

Starting from version 2.1, IPF provides basic support for IHE transactions with numbers 44-47. They are in fact counterparts for PIX/PDQ transactions discussed in the previous section, but use HL7 v3 instead of v2 as data format and Web Services instead of MLLP as transport protocol.

Standard conformance

Normative description for PIXv3/PDQv3 transaction can be found in the corresponding IHE Supplement for Trial Implementation.

Note that ATNA logging is not defined there yet, therefore the IPF does not log any interaction events related to PIXv3/PDQv3.

Configuration

pom.xml must contain the following dependency:

<dependency>
    <groupId>org.openehealth.ipf.platform-camel</groupId>
    <artifactId>platform-camel-ihe-pixpdqv3</artifactId>
    <version>${ipf-version}</version>
</dependency>

PIXv3/PDQv3 transactions use the same Web Service machinery as XDS.b transactions, therefore they must be configured in the same way. Sections "Exposing a service" and "Call a service" apply respectively, too.

There are two variants of ITI-44 endpoints — one for the PIX Identifier Cross-Reference Manager (Section 3.44.4.1.3.1 of the aforementioned IHE Supplement) and another one for the XDS.b Document Registry (Section 3.44.4.1.4.1). This leads to the existence of two pretty similar components in the IPF, namely pixv3-iti44 and xds-iti44, which differ in naming conventions and namespaces in WSDL documents.

Because of unsupported ATNA auditing, the only available URL parameter is secure.

Message types

Currently, PIXv3/PDQv3 components do not use any special data model and expect both request and response to be String representations of XML payload. Web Service metadata (WSDL documens) reflects this peculiarity by declaring all message parts to be xsd:anyType.

IPF's DSL extensions for XML processing can be efficiently used to handle and prepare request and response messages.

Validation

Validation of PIXv3/PDQv3 request and response messages is currently XSD-based. Same as for other transactions, there are corresponding DSL extensions pre-defined, which can be activated for any given IPF application using the following Spring configuration:

...
<bean id="pixpdqv3ModelExtension"
      class="org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.PixPdqV3ModelExtension"/>

<bean id="routeModelExtender"
      class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
    <property name="routeModelExtensions">
        <list>
            <ref bean="pixpdqv3ModelExtension" />
            <!-- references to other extension beans -->
        </list>
    </property>
</bean>
...

After that, requests and responses can be validated as outlined in the example route below:

from("pixv3-iti44:iti44service")
    .onException(Exception.class)
        .maximumRedeliveries(0)
        .end()
    .validate().iti44Request()
    .unmarshal().gnode()
    .process {
         // prepare response as String
         // <MCCI_IN000002UV01 xmlns="urn:hl7-org:v3" ...>
         //     <id root="PID-3-4-2" extension="PID-3-1" />
         //     ...
         // </MCCI_IN000002UV01>
     }
    .validate().iti44Response()

Translation between HL7 v2 and HL7 v3 message models

Preliminary contents

Informations provided in this chapter relate to an early-access functionality, which may be changed out of all recognition in the near future.

IPF 2.1 contains utilities for translation between HL7 v2 and HL7 v3, thus giving the possibility to implement HL7 v3 transactions on top ot their HL7 v2 counterparts and to avoid redundancy in that way. Currently supported transaction pairs are PIX Feed (ITI-8/ITI-44), PIX Query (ITI-9/ITI-45), PIX Update Notification (ITI-10/ITI-46), and PDQ (ITI-21/ITI-47).

Configuration and Use

This section describes configuration steps the user has to perform in order to use the HL7 translators, and provides instructions how to perform the translation. The next section contains an example of a bridge route.

Step 1

First of all, the corresponding Maven artefact must be referenced as a dependency in the application's pom.xml, as described above. (Translation utilities per se belong to the artefact commons-ihe-pixpdqv3, which will be loaded transitively from platform-camel-ihe-pixpdqv3.)

Step 2

For translation of PIX Feed and PDQ messages, the IPF Mapping Service must be activated and configured to use the mappings provided by the IPF (these mappings can be accessed as classpath resources). Here is a snippet of Spring-based configuration:

<bean id="mappingService" class="org.openehealth.ipf.commons.map.BidiMappingService">
    <property name="mappingScripts">
        <list>
            <!-- for PIX Feed message translation -->
            <value>classpath:META-INF/map/pixfeed-translation.map</value>
            <!-- for PDQ message translation -->
            <value>classpath:META-INF/map/pdq-translation.map</value>
            <!-- other mappings -->
            <value>...</value>
        </list>
    </property>
</bean>

<bean id="hapiModelExtension"
      class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
    <property name="mappingService" ref="mappingService" />
</bean>

<bean id="mappingModelExtension"
      class="org.openehealth.ipf.commons.map.extend.MappingExtension">
    <property name="mappingService" ref="mappingService" />
</bean>

<bean id="pixpdqv3ModelExtension"
      class="org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.PixPdqV3ModelExtension" />

<bean id="coreModelExtension"
      class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension" />

<bean id="routeModelExtender"
    class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
    <property name="routeModelExtensions">
        <list>
            <ref bean="coreModelExtension" />
            <ref bean="hapiModelExtension" />
            <ref bean="mappingModelExtension" />
            <ref bean="pixpdqv3ModelExtension" />
            <ref bean="..." />
        </list>
    </property>
</bean>

Step 3

The third configuration step is to instantiate translator beans.
Package org.openehealth.ipf.commons.ihe.pixpdqv3.translation contains the following set of classes:

  • PixFeedRequest3to2Translator — translates PIX Feed requests from v3 to v2.
  • PixFeedAck2to3Translator — translates PIX Feed acknowledgements (responses) from v2 to v3.
  • PixQueryRequest3to2Translator — translates PIX Query requests from v3 to v2.
  • PixQueryResponse2to3Translator — translates PIX Query responses from v2 to v3.
  • PixUpdateNotification2to3Translator — translates PIX Update Notifications from v2 to v3.
  • PixUpdateNotificationAck3to2Translator — translates PIX Update Notification acknowledgements from v3 to v2.
  • PdqRequest3to2Translator — translates Patient Demographic Queries from v3 to v2.
  • PdqResponse2to3Translator — translates Patient Demographic Responses from v2 to v3.

Each translator has a set of configurable properties. Their descriptions can be currently taken from javadoc of the corresponding classes. Note also that there are reasonable default values, therefore the explicite configuration (see <property> items in the example below) can be harmlessly omitted in many cases.

An example for PIX Feed:

<bean name="pixFeedRequestTranslator"
      class="org.openehealth.ipf.commons.ihe.pixpdqv3.translation.PixFeedRequest3to2Translator">
    <property name="useSenderDeviceName"      value="true" />
    <property name="useReceiverDeviceName"    value="true" />
    <property name="copyEmailAs"              value="PID-13-1" />
    <property name="copyAccountNumberAs"      value="PID-18" />
    <property name="accountNumberRoot"        value="1.2.3" />
    <property name="copyNationalIdentifierAs" value="PID-19" />
    <property name="nationalIdentifierRoot"   value="2.16.840.1.113883.4.1" />
    <property name="birthNameCopyTo"          value="PID-5" />
    <property name="useOtherIds"              value="true" />
</bean>

<bean name="pixFeedAckTranslator"
      class="org.openehealth.ipf.commons.ihe.pixpdqv3.translation.PixFeedAck2to3Translator">
    <property name="ackCodeFirstCharacter" value="C" />
</bean>

Step 4

Package platform-camel-ihe-pixpdqv3, being the basis for the PIXv3 and PDQv3 transactions' impementation, provides DSL extensions which can be used to embed HL7 translation functionality into a Camel route. Spring configuration performed on Step 2 already contains all necessary statements to activate these extensions — consider the bean pixpdqv3ModelExtension.

Using DSL Extensions

The DSL extension .translateHL7v3toHL7v2(translator) is responsible for the translation of messages from HL7 v3 to HL7 v2. Its parameter translator is the corresponding translator bean, e.g. it could be the bean pixFeedRequestTranslator defined on Step 3. Moreover, this DSL extensions saves the original HL7 v3 request internally, because some parts of it will have to be yielded unmodified into the HL7 v3 response message.

Translation of response messages from HL7 v2 to HL7 v3 is the task of the DSL extension .translateHL7v2toHL7v3(translator). The parameter translator denotes a response translator bean, e.g. pixFeedAckTranslator defined on Step 3.

When the translation of a response message was not preceded by the translation of the corresponding request message and therefore the request could not been saved automagically by means of IPF's internal machinery, the user has to provide the request manually (as a String containing XML document or a MessageAdapter instance depending on the transaction under consideration) in the property org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.HL7V3_ORIGINAL_REQUEST_PROPERTY of the Camel exchange.

Example

Here is a sample Camel route that impements bridges PIX Feed v3 requests (ITI-44) to an HL7 v2-based Patient Identifier Cross-Reference Manager (ITI-8), and does the same in reverse direction for acknowledgements.

class BridgeRouteBuilder extends SpringRouteBuilder {
    // injected by Spring
    def pixFeedRequestTranslator
    def pixFeedAckTranslator
    def pixManagerUri

    void configure() throws Exception {
        from('pixv3-iti44:iti44service')
            .onException(Exception.class)
                .maximumRedeliveries(0)
                // some reasonable exception handling
                .end()
            .translateHL7v3toHL7v2(pixFeedRequestTranslator)
            .to("pix-iti8://${pixManagerUri}")
            .translateHL7v2toHL7v3(pixFeedAckTranslator)
    }
}

This is, probably, the shortest possible HL7v3 PIX Manager implementation .

ATNA

ATNA auditing functionality is fully integrated into the corresponding IPF IHE components (excluding PIXv3/PDQv3 ones). The only thing the user has to configure is the URI of the target syslog server, as described in the next section.

Configuring auditors

Each of the currently supported IHE actor types has a corresponding singleton auditor, i.e. the following set is available:

  • XDSRegistryAuditor
  • XDSRepositoryAuditor
  • XDSSourceAuditor
  • XDSConsumerAuditor
  • PIXManagerAuditor (serves the Patient Demographic Supplier actor as well)
  • PIXSourceAuditor
  • PIXConsumerAuditor
  • PDQConsumerAuditor

Auditors can be configured both individually and all together. Besides the syslog server URI mentioned above (actually the only mandatory parameter), the following optional parameters can be set up (see Section 5.4 of RFC 3881 for details):

  • audit source ID
  • audit enterprise site ID
Warning

Configuration of auditors is stored in instances of the class org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig. These instances contain much more configurable fields than the three described here, but the user is actually not ought to change them.

Individual configuration

To configure a particular auditor (for example, the XDS Registry-related one), the user can write

import org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig;
import org.openhealthtools.ihe.atna.auditor.XDSRegistryAuditor;
...

AuditorModuleConfig config = new AuditorModuleConfig();
config.setAuditRepositoryHost("my.syslog.server");
config.setAuditRepositoryPort(514);
config.setAuditSourceId(...);
config.setAuditEnterpriseSiteId(...);
XDSRegistryAuditor.getAuditor().setConfig(config);

To perform the same operation using Spring, the following obvious bean definitions can be used:

<bean id="config"
      class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig">
    <property name="auditRepositoryHost"       value="my.syslog.server" />
    <property name="auditRepositoryPort"       value="514" />
    <property name="auditSourceId"             value="..." />
    <property name="audituditEnterpriseSiteId" value="..." />
</bean>

<bean id="registryAuditor"
      class="org.openhealthtools.ihe.atna.auditor.XDSRegistryAuditor"
      factory-method="getAuditor">
    <property name="config" ref="config" />
</bean>

Group configuration

From a Java application:

import org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext;
...

AuditorModuleContext.getContext().getConfig().setAuditRepositoryHost("my.syslog.server");
AuditorModuleContext.getContext().getConfig().setAuditRepositoryPort(514);

Using Spring descriptor:

<bean id="iheAuditorContext"
      class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext"
      factory-method="getContext">
</bean>

<bean id="iheAuditorConfig"
      factory-bean="iheAuditorContext"
      factory-method="getConfig">

    <property name="auditRepositoryHost" value="my.syslog.server" />
    <property name="auditRepositoryPort" value="514" />
</bean>

Disabling auditing

In order to disable auditing for a particular service endpoint (i.e. a Camel consumer) or for a particular client invocation (i.e. a Camel producer), the corresponding URL should be extended with parameter audit=false, e.g.

to('xds-iti18://localhost:9091/xds-iti18-service?audit=false')

or

from('xds-iti18:xds-iti18-service?audit=false')

Note that this feature is (currently) not supported for ITI-17.

Writing down incomplete audit records

Under some circumstances (for example, when the request does not contain all required elements) the system is not able to collect all necessary data to construct a well-formed audit record. Per default, no audit is performed in this case, because it would violate the specification.

But — as an exception, e.g. for debug purposes — the user can change this behavior by setting the parameter allowIncompleteAudit in the URL of the corresponding XDS to true:

to('xds-iti18://localhost:9091/xds-iti18-service?allowIncompleteAudit=true')

Of course, this setting will not have any effect when the auditing functionality is generally switched off, i.e. when the parameter audit in the mentioned endpoint URL was set to false, as described in the previous section.

Note that this feature is (currently) not supported for ITI-17.

Configure auditing transport: Reliable and special auditing

Default implementation of ATNA auditing is based on unreliable UDP communication (Syslog protocol), as prescribed by the IHE IT TF, Vol. 2, Section 3.20.6.1. This choice is also explained in the ATNA FAQ.

In order to change this setting, a custom implementation of org.openhealthtools.ihe.atna.auditor.sender.AuditMessageSender must be provided and registered via AuditorModuleContext.getContext().setSender(mySender).

The delivery queue can be customized in a similar way, i.e. by implementing the interface org.openhealthtools.ihe.atna.auditor.queue.AuditMessageQueue and installing the corresponding class instance via AuditorModuleContext.getContext().setQueue(myQueue).

These settings will affect all auditors, because the auditor module context is a singleton.

Routing audit messages to Camel endpoints

This section is an application of the previous one. Instead of sending audit messages to a syslog server they can also be sent to Camel endpoints. For that purpose an CamelEndpointSender must be configured in the application context.

context.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd">

    <camel:camelContext id="camelContext">
        <camel:routeBuilder ref="..."/>
    </camel:camelContext>

    <bean id="auditModuleConfig" factory-method="getConfig"
        factory-bean="auditModuleContext">
        <property name="auditRepositoryHost" value="0.0.0.0" />
        <property name="auditRepositoryPort" value="0" />
    </bean>

    <bean id="auditModuleContext" factory-method="getContext"
        class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext">
        <property name="sender" ref="camelEndpointSender" />
    </bean>

    <bean id="camelEndpointSender"
        class="org.openehealth.ipf.platform.camel.ihe.atna.util.CamelEndpointSender">
        <!-- autowired if not explicitly configured -->
        <property name="camelContext" ref="camelContext" />
        <!-- endpoint registered in the Camel context -->
        <property name="endpointUri" value="direct:input" />
    </bean>

    ...

</beans>

In this example a CamelEndpointSender is configured to send audit messages to the direct:input endpoint that is registered at the Camel context. The AuditorModuleContext singleton is configured to use the custom audit message sender. Although ignored by the sender, the Open Health Tools require us to set the audit repository host and port. Hence, they can be set to arbitrary values, 0.0.0.0 and 0, respectively, in our example. These values are overwritten by the message sender. The sender derives them from the Camel endpoint URI. For exmple, direct:input yields 0.0.0.0 and -1, http://www.openehealth.org:7000 yields 174.143.206.54 and 7000.

The audit message is sent as in-only exchange to the endpoint where the in-message body contains a string-representation of the audit XML message. Additionally the following headers are defined.

Header Description
org.openehealth.ipf.platform.camel.ihe.atna.datetime The date and time when this message was generated.
org.openehealth.ipf.platform.camel.ihe.atna.destination.address The audit repository IP address derived from the configured endpoint URI.
org.openehealth.ipf.platform.camel.ihe.atna.destination.port The audit repository port derived from the configured endpoint URI.

The CamelEndpointSender is contained in the platform-camel-ihe-atna-util component which needs to be added as dependency in the Maven pom.xml. Adjust the version accordingly depending on the latest IPF release.

pom.xml
<dependencies>
    <dependency>
	<groupId>org.openehealth.ipf.platform-camel</groupId>
	<artifactId>platform-camel-ihe-atna-util</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
</dependencies>

URL Parameters' Summary for XDS and PIXv3/PDQv3 transactions

The following table gives an overview of all URL parameters defined for IPF XDS and PIXv3/PDQv3 endpoints (ITI-14..18, 41..47):

Name Type Default value Transaction numbers Side Short description
secure boolean false all except 17 client only whether Transport Layer Security mechanisms should be applied when sending messages
audit boolean true all except 17 and 44-47 both service and client whether ATNA auditing should be performed by the corresponding party
allowIncompleteAudit boolean false all except 17 and 44-47 both service and client whether incomplete audit records should be sent to the ATNA repository as well
soap11 boolean false 18 and 41-43 client only whether requests should be sent using SOAP 1.1 instead of SOAP 1.2
(not supported any more since 2010-02-10)

CDA support

Early access

Please note that the following documentation and the underlying code for CDA support is early access and might be subject to change.

Support for CDA processing in IPF is provided by several IPF components. This is summarized in the following table.

Component Description Documentation
modules-cda Provides functionality for creating, parsing, rendering, and validation CDA documents Generic CDA Support
platform-camel-cda Provides HL7 extensions to the Camel DSL. DSL Extensions

Clinical Document Architecture - a brief overview

The HL7 Clinical Document Architecture (CDA) is an XML-based markup standard intended to specify the encoding, structure and semantics of clinical documents for exchange. CDA is part of the HL7 version 3 standard.
By the use of XML, the HL7 v3 standard and coded vocabularies, CDA allows for the exchange of documents that are both machine and human-readable enabling electronic processing for decision support etc whilst being easily retrieved and used by the people who need them.

Support for 'vanilla' CDA

IPF provides support for

  • creating CDA document objects from scratch
  • rendering a CDA document object into its XML representation
  • parsing of existing CDA documents
  • extracting individual pieces of information from CDA documents

As in IPF's HL7 module, support really means more than just providing some sort of CDA API for its model and services. CDA documents can be created and analyzed by means of a domain-specific language (DSL) that hides away most of the technical details you usually encounter when dealing with complex XML documents.

CDA specification

Even though some technical details are hidden, the domain-specific details are not (at least for the generic CDA support). Be sure to read the CDA specification and have a printed copy of the CDA R-MIM at hands while working with CDA documents.

Support for CDA content profiles

In a technical sense, a CDA content profile defines a set of constraints on CDA that define how to use the CDA to communicate clinical documents bound to a certain use case, e.g. clinical summaries. CDA content profiles are also often referred to as CDA Implementation Guides.
The first content profile to be supported is the Continuity of Care Document (CCD) profile, as it serves as a baseline for many other profiles published by standard bodies like HL7, IHE, or HITSP.

Generic CDA support

CDA support is assembled from a variety of sources.

  • The underlying CDA object model, parser, and renderer is provided by Open Health Tools (OHT)'s IHE Profiles project. The OHT libraries are redistributed as part of IPF's CDA support.
  • Tooling to create and validate CDA documents are provided natively by IPF.
  • CDA Parser, Renderer and Validator are adapted to implement the Module Adapters of IPF. This ensures, that they can be used as processors in Camel-based integration routes.
Use of IPF CDA without Apache Camel

IPF's generic CDA support has no dependencies on Apache Camel and can therefore as well be used independently of integration solutions based on Apache Camel.

The CDA builder used to create CDA documents is strongly based on Groovy's Builders and defines its own kind of domain specific language. Compared to "traditional" APIs, the natural hierarchical syntax makes it very easy to assemble parts of a CDA document or a whole CDA document, while at the same time enforces a significant amount of restrictions defined by the specification.

Background info: MetaBuilder

CDA builder uses and is derived from MetaBuilder, a more sophisticated builder implementation, which allows for a declarative builder implementation. In fact, the CDA Builder itself is defined using MetaBuilder and Groovy meta class programming.

Configuration

For setting up Maven follow the instructions on the IPF development page. If you want to use the CDA Features 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-cda</artifactId>
<version>${ipf-version}</version>
</dependency>

For using the CDA support inside Camel routes you need to include the following dependency:

pom.xml for use with Camel
<dependency>
<groupId>org.openehealth.ipf.platform-camel</groupId>
<artifactId>platform-camel-cda</artifactId>
<version>${ipf-version}</version>
</dependency>

where ${ipf-version} must be replaced with the IPF version you want to use.

Like the HL7 DSL, IPF adds a couple of Groovy metaclass extensions on top of the underlying CDA Object model to facilitate accessing CDA documents. You can register these extensions manually:

import org.openehealth.ipf.modules.cda.builder.CDAR2ModelExtension
...
ExpandoMetaClass.enableGlobally()
new CDAR2ModelExtension().extensions.call()

Usually you would use a Spring ApplicationContext to register the extensions, especially in conjunction with Camel routes:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:camel="http://camel.apache.org/schema/spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://camel.apache.org/schema/spring 
http://camel.apache.org/schema/spring/camel-spring.xsd">


<camel:camelContext id="camelContext">
   <camel:routeBuilder ref="routeBuilder"/>
</<camel:camelContext>

...

<bean id="cdaModelExtension"
   class="org.openehealth.ipf.modules.cda.builder.CDAR2ModelExtension">
</bean>

<bean id="routeModelExtender"
   class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
   <property name="routeModelExtensions">
      <list>
         ...
         <ref bean="cdaModelExtension" />
      </list>
   </property>
</bean>

Creating generic CDA documents

The basic pattern of how to create a CDA document is

import org.openehealth.modules.cda.builder.CDAR2Builder
....
def builder = new CDAR2Builder()
def document = builder.build {
                  clinicalDocument {
                     // add Header information
                     // add Body information
                  }

Header and Body information is defined by a sequence of nested elements, which are described in more detail below.

Note that the CDA builder does not assemble the XML document directly. Instead it builds up an internal model of the CDA document; rendering to XML is a seperate step (see section on Parsing and Rendering). This comes with a number of advantages:

  • the order of the elements built within an hierarchy level does not matter
  • you don't have to care about the type of an element - the builder knows that e.g. a code attribute inside a ClinicalDocument is of HL7 RIM type CE.
  • unknown elements or attributes are detected as well as wrong cardinalities etc.
  • the builder itself comes with further rules that are covered by neither the CDA XML schema nor the underlying object model, e.g. that codes of type CE must either have a nullFlavor set or at least a code attribute.
  • there are shortcut notations for a less verbose syntax, in particular with respect to simple types and enumerated values, e.g. a HL7 RIM PQ type can be instantiated by string consisting of value and unit. Thus, pq(value:86.0, unit:'kg') and pq('86 kg') are equivalent.

Groovy builders

Builders are based on the builder pattern from the GOF design pattern book. It provides a way to build your own DSL and represents a powerful concept in Groovy.
To put it short, Groovy builders allow to build up a hierarchical structure of nodes, while each node may have attributes (i.e. a set of key/value pairs) and content. Obviously, this matches very well with XML documents, thus also for CDA.
IPF's CDA support uses MetaBuilder, that adds an extra layer on top of plain Groovy builders.

Instantiate CDAR2Builder once!

Instantiating org.openehealth.modules.cda.builder.CDAR2Builder instances is very expensive. It can take several seconds of ramp-up time to initialize all builder rules defined for generating CDA documents.
Therefore it is essential to create CDAR2Builder as a singleton object, e.g. by defining it as a bean in a Spring ApplicationContext.
Fortunately, using a CDAR2Builder object has been made thread-safe, so you can share the instance to generate CDA documents concurrently is several threads.

The general syntax for creating a CDA element is defined recursively. The [brackets] contain optional syntax.

top-element = schema-name([ value ], [ attributes ]) [ nested-elements ]
elements = element [ elements ]
element = name([value], [ attributes ]) [ nested-elements ]
nested-elements = '{' elements '}'
attributes = attribute [, attributes ]
attribute = key-name : value

The syntax elements (printed in italics) is listed below;

syntax element description examples
schema-name The schema (class) of the outermost CDA element that is created in the builder statement. schema-name often corresponds closely with the HL7v3 RIM type of the corresponding XML element name. clinicalDocument, section, ii
name Name of a CDA element. name often corresponds closely with the the corresponding XML element name. author, id, code
key-name Name of a nested CDA element or attribute. Used when the element or attribute is provided in the value instead of being recursively built root, code
value A valid Groovy expression that returns a object reference. Can also be a primitive value 57, '1976', object

Let's start with how the header information of a CDA document is assembled.

CDA Header

Building the CDA header also explains the different kinds of elements and attributes and how they a created using the CDA builder.

def document = builder.build {
  clinicalDocument {
  // templateId(root:'2.16.840.1.113883.3.27.1776')        // 1
     id(root:'2.16.840.1.113883.19.4', extension:'c266')   // 2
     code(                                                 // 3
        code:'11488-4', 
        codeSystem:'2.16.840.1.113883.6.1', 
        codeSystemName:'LOINC',
        displayName:'Consultation note'
     )
     effectiveTime('20000407')                             // 4
     title('Good Health Clinic Consultation Note')         // 5
     versionNumber(2)                                      // 6
     ...
  }
}

clinicalDocument is the schema-name, containing a closure to nest other elements. id is the name for the first element - the CDA Builder knows the internal structure of a CDA document and creates an object of the HL7v3 RIM type II. root and extension are the key-name for the corresponding attributes of a II element, and they are assigned the value '2.16.840.1.113883.19.4' and 'c266', respectively.

Let's examine this piece of code line by line

  1. In general, infrastructure attributes (templateId, classCode, moodCode, typeCode) with static defaults are not required to be set. Generic CDA documents always carry the same templateId, so you can omit this element.
  2. This adds an id element with a root and extension attribute. Under the hood, the builder checks that the root attribute is present, otherwise an Exception is thrown. Also note that id is mandatory, so omitting would throw an Exception, too.
  3. Adding a code just works like adding the id. Remember, you never have to care about the class of the corresponding CDA model object.
  4. Looking at the CDA specification, effectiveTime and title appear in inverted order. As we are not building the XML document directly, we don't have to care, as the rendering process restores the correct ordering of elements. effectiveTime requires a date value (HL7 TS datatype), but the builder allows to use its simple string representation (YYYYMMDD).
  5. The title is of HL7 ST datatype. Again, you can just pass a string and conversion is done for you.
  6. The versionNumber is of HL7 INT datatype. Here you can pass an integer value.

The next part of the CDA header definition introduces a couple of other 'shortcuts' available.

...
     confidentialityCode('N')                           // 1
//      code:'N', 
//      codeSystem:'2.16.840.1.113883.5.25')
     recordTarget {                                     // 2
        patientRole {
           id('12345@2.16.840.1.113883.19.5')           // 3
//            extension='12345' 
//            root='2.16.840.1.113883.19.5'
           patient {
              name {
                 given('Henry') 
                 family('Levin')
                 suffix('the 7th')
              }
              administrativeGenderCode('M')             // 4
//               code='M' 
//               codeSystem='2.16.840.1.113883.5.1'

              birthTime('19320924')
           }
           providerOrganization {
              id('2.16.840.1.113883.19.5')              // 5 
           }
        }
    }
...
  1. Codes with more or less fixed code systems have their OID and, if applicable, code system name predefined. In this case, you can specify the code as content and skip the codeSystem and codeSystemName attributes. The rendered CDA XML will nevertheless contain all attributes.
  2. In this block, we traverse the RIM model from the document act over a Participation (recordTarget) and a Role (patientRole) to the Entity (patient).
  3. id elements can be created by specifying a 'extension@root' content rather than the attribute map as shown above. Both notations are equivalent.
  4. This is another example for a predefined code system.
  5. id elements may contain only a root attribute, but no extension. In this case, you can provide the root attribute as content.

For reference purposes, here's the remainder of a rather complete CDA header definition. Note that most of the elements are optional - check the CDA specification or RMIM diagram. If any mandatory elements are skipped, the CDA Builder will complain with an Exception before you get the clinical document as result.

...
           author {
             time('2000040714')
             assignedAuthor {
                id(extension:'KP00017',root:'2.16.840.1.113883.19.5')
                assignedPerson {
                   name {
                      given('Robert')
         	          family('Dolin')
                      suffix('MD')
             	   }
                }
                representedOrganization {
                   id(root:'2.16.840.1.113883.19.5')
                }
             }
          }
             	
          custodian {
             assignedCustodian {
                representedCustodianOrganization {
             	   id('2.16.840.1.113883.19.5')
             	   name('Good Health Clinic')
             	}
             }
          }
          legalAuthenticator {
             time('20000408')
             signatureCode('S')
             assignedEntity {
                id(extension:'KP00017', root:'2.16.840.1.113883.19.5')
             	assignedPerson {
             	   name {
         	          given('Robert')
         	          family('Dolin')
         	          suffix('MD')
             	   }
             	}
             	representedOrganization {
             	   id('2.16.840.1.113883.19.5')
             	 }
              }
           }
           relatedDocument(typeCode:'APND') {                                    // 1
              parentDocument {
                 id(extension:'a123', root:'2.16.840.1.113883.19.4')
             	 setId(extension:'BB35', root:'2.16.840.1.113883.19.7')
             	 versionNumber(1)
              }
           }
           componentOf {
              encompassingEncounter {
                 id(extension:'KPENC1332', root:'2.16.840.1.113883.19.6')
             	 effectiveTime {
             	    low('20000407')
             	 }
             	 encounterParticipant(typeCode:'CON') {
             	    time('20000407')
             	    assignedEntity {
             	       id(extension:'KP00017',root:'2.16.840.1.113883.19.5')
             	       assignedPerson {
             	          name {
             	             given('Robert')
             	             family('Dolin')
             	             suffix('MD')
             	          }
             	       }
                       representedOrganization {
                          id(root:'2.16.840.1.113883.19.5')
                       }
             	    }
             	 }
             	 location {
             	    healthCareFacility {
             	       code(
             	           code:'GIM',
             	           codeSystem:'2.16.840.1.113883.5.10588',
             	           displayName:'General internal medicine clinic'
             	       )
             	    }
             	 }
             }
         }
     }
  }

CDA body

The body of a CDA document is created correspondingly like the header part.
The CDA body can be either an unstructured blob, or can be comprised of structured markup. Every CDA document has one body at most - associated with the ClinicalDocument class through the component relationship -, which can be either non-structured or structured.

nonXMLBody() represents a document body that is in some format other than XML, e.g. a image of PDF document. nonXMLBody.text is used to reference data that is stored externally to the CDA document or to encode the data directly inline.

...
component {
   nonXMLBody {
        text(
                mediaType: 'application/pdf',
                representation: 'B64',
                'JVBERi0xLjMKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdG...'
        )
   }
}
...

structuredBody() represents an XML document body that is comprised of one or more document sections. Document sections can nest, can override context propagated from the header, and can contain narrative and CDA entries.

...
component {
   structuredBody {
        component {        // Section 1
              section {
                 ...
              }
        }
        component {        // Section 2
              section {
                 ...
              }
        }
        ...                // ...
    }
}
...
Narrative Block

section.text is used to store a narrative text and is therefore referred to as the CDA Narrative Block.
For structured bodies, it is the document originator's responsibility to properly populate the Narrative Block, regardless of whether information is also conveyed in CDA entries. Vice versa, it is the recipient's responsibility to properly render the narrative block in human readable manner, e.g. using a XSL transformation.

The CDA DSL for the Narrative Block follows exactly chapter 4.3.5 of the CDA specification. See the examples below.

Simple narrative text
...
section {
   title('Some title'}
   text('A simple narrative content')
   ...
}
...
List narrative text
...
section {
   title('Some title'}
   text {
       list {
          item('Theodur 200mg BID')
          item('Proventil inhaler 2puffs QID PRN')
          item('Prednisone 20mg qd')
          item('HCTZ 25mg qd')
       }
   }
   ...
}
...
Complex narrative text
...
section {
   title('Some title'}
   text {
        paragraph('Payer information')
        table(border: '1', width: '100%') {
          thead {
            tr {
              th('Payer name')
              th('Policy type')
              th('Covered Party ID')
              th('Authorizations')
            }
          }
          tbody {
            tr {
              td('Good Health Insurance')
              td('Extended healthcare / Self')
              td('14d4a520-7aae-11db-9fe1-0800200c9a66')
              td {
                linkHtml(href: 'Colonoscopy.pdf', 'Colonoscopy')
              }
            }
          }
        }
   }
   ...
}
...
Structured part

Sections can define participants like author, informant, and subject (i.e. the primary target of the recorded entries). Sections can also have relationships to entries, which contain structured computer-processable components. Each section can contain zero to many entries. There is a number of entry classes:

  • act
  • encounter
  • observation
  • observationMedia
  • organizer
  • procedure
  • regionOfInterest
  • substanceAdministration
  • supply

Entries in return can have participants and relationships to other entries. For details on these entry classes, please refer to the CDA R2 specification.

With CDA Builder you create the structured part following the same patterns as with the CDA Header of Narrative Block. The following example creates a substanceAdministration entry.

...
structuredBody {
    component {
        section {
            title('My title')
            text('My narrative text')
            code(....)
            entry {
                substanceAdministration(classCode:'PROC', moodCode:'EVN') {
                    consumable{
                        manufacturedProduct {
                            manufacturedLabeledDrug {
                                code(
                                    code:'10312003',
                                    codeSystem:'2.16.840.1.113883.6.96' ,
                                    codeSystemName:'SNOMED CT',
                                    displayName:'Prednisone preparation')
                            }
                        }
                    }
                }
            }
          ...
        }
    }
   ...
}

Sections and their contained participants and entries can get arbitrarily complex. Furthermore, the structure is very generic, i.e. it's possible to express a certain clinical concept in a variety of ways. Unconstrained CDA is therefore not very helpful when it comes to real semantic interoperability. To be useful in the real world, CDA has to be constrained, which can happen on at least two levels:

  • using section-level templates
  • using entry level templates

The RIM's InfrastructureRoot class contains an attribute, templateId, which is available for use in CDA. Thus, CDA provides a mechanism to reference a template or implementation guide that has been assigned a unique template identifier.

CDA templates are usually collected in CDA content profiles, one of which is the CCD (Continuity of Care Document).

CDA builder tips

Including complete parts

With CDA Builder it's possible to construct a complete document in a single builder statement. As CDA documents are built from several very well separated parts, it might as well make sense to create such a part for itself and include it into the final document in a separate step. This way, a document part can also be reused for several CDA documents.
CDA builder supports this strategy out of the box. The cardinality of such a part within its containing element plays an important role.

Single cardinality

In this example, we create a element of type CE and include it into a code element inside a Supply clinical statement. Alternatively, we can also build the code inline:

...
def myCode = builder.build { 
   ce(code:'30549001', 
      codeSystem:'2.16.840.1.113883.6.96', 
      displayName:'Suture removal') 
}

// Assign code as attribute
def myEntry = builder.build {
   entry {
      supply(classCode:'SPLY', moodCode:'EVN', code:myCode) {
         statusCode(code:'completed')
         effectiveTime(value:'200004071430')
      }
   }
}

// Alternative: build myCode inline
myEntry = builder.build {
   entry {
      supply(classCode:'SPLY', moodCode:'EVN') {
         code(code:'30549001', 
              codeSystem:'2.16.840.1.113883.6.96', 
              displayName:'Suture removal') 
         statusCode(code:'completed')
         effectiveTime(value:'200004071430')
      }
   }
}
...

The difference is that myCode is assigned as an attribute like classCode or moodCode is, while when being built inline it is nested under the supply element. Also note that for pre-constructing myCode you need to know the schema name of its HL7v3 type (ce in this case), while when being built inline the CDA Builder knows to instantiate a CE object for a Supply code.

Multiple cardinality

In this case, the complete part must be added to its container collection instead of just being assigned to it. Unfortunately, this is currently not possible within a CDA builder statement.

...
def mySection = builder.build {
   section {
      title('My section')
   }
}
mySection.entry.add(myEntry)
...
Variable-typed values

In some cases, the CDA specification defines attributes to be of either any type or any subtype of a certain type. Most importantly, the value of an observation clinical statement is variable-typed. As another example, the effectiveTime of a substanceAdministration is of type GTS, which can be a range of different timing sub-types.
In these cases, obviously, the CDA builder can not infer the exact type from a attribute name, just because it's variable. Therefore, you have to give a hint, as shown in the following fictive example:

def myObservationEntry = builder.build {
   entry {
      observation {
         id('9d3d416d-45ab-4da1-912f-4583e0632000')
         ....
         value(
            make {
               snomedCode(code:'40275004', displayName:'Contact dermatitis') {
                  translation(
                     code: '692.9',
                     codeSystem: '2.16.840.1.113883.6.2', 
                     codeSystemName: 'ICD9CM',
                     displayName: 'Contact Dermatitis, NOS')
               }
            }
         )
         value(
            make { _int(10) }  // int is a reserved Groovy keyword, 
                               // so we have to use _int
         )
         ...
      }
   }
}

This observation has two values: a (SNOMED) code and an integer. You simply wrap the type into a make element. In fact, make is more or less an abbreviation of builder.build, i.e. you create an object of the desired type and assign it to the variable-typed attribute.

Using regular Groovy code inside CDA builder

As with the HL7 DSL of IPF, the Groovy Builder is an internal DSL, i.e. it is expressed by means of the Groovy programming language and can be executed without an additional parser. Therefore, it is also possible to mix and match CDA builder code with regular Groovy code.
The following example shows how to derive a Medication section from a tabular data structure, using loops, conditiional statements, and debugging output.

...
    // Some medication data, stored in a list of maps
    def data = [
	
	[medication:'Albuterol inhalant',
	instructions:'2 puffs QID',
	startDate:null,
	period:'6 h',
	routeCode:'IPINHL',
	dose:'2',
	administrationUnitCode:'415215001',
	medicationCode:'307782',
	id:'cdbd33f0-6cde-11db-9fe1-0800200c9a66'],
	
	[medication:'Prednisone',
	instructions:'20mg PO daily',
	startDate:'20000328',
	period:'24 h',
	routeCode:'PO',
	dose:'1',
	medicationCode:'312615',
	id:'cdbd5b03-6cde-11db-9fe1-0800200c9a66'] 
	
  ]
...
  POCDMT000040Section section = builder.build {
    section {
      templateId('2.16.840.1.113883.10.20.1.8')
      code('10160-0@2.16.840.1.113883.6.1')
      title('Medications')
      text {
        table(border:'1',width:'100%') {
          thead {
             tr {
               th('Medication')
               th('Instructions')
               th('Start date')
             }
          }
          tbody {
            // Iterate over all medications. Must assign a iteration variable!
            data.each { m ->
              tr {
                td(m.medication)
                td(m.instructions)
                td(m.startDate ?: '') // Avoid 'null' output
              }
            }
          }
        }
      }
      // Iterate over all medications. Must assign a iteration variable!
      data.each { m ->
        // Insert diagnostic output...
        println "Creating medication " + m.medication
        entry {
          substanceAdministration(classCode:'SBADM', moodCode:'EVN'){
            id(m.id)
            // Conditional element. Skip if not available
            if (m.startDate) {
              effectiveTime(make {
                pivlts { period(m.period) }
              })
            }
            routeCode(code:m.routeCode, codeSystem:'2.16.840.1.113883.5.112')
            doseQuantity(m.dose)
            consumable {
              manufacturedProduct {
                manufacturedLabeledDrug {
                  code(code:m.medicationCode,
                       codeSystem:'2.16.840.1.113883.6.96') { 
                    originalText(m.medication) 
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  Assert.assertNotNull(section)
  // We have two sections ...
  Assert.assertEquals 2, section.entry.size()
  // with individual IDs ...
  Assert.assertEquals 'cdbd33f0-6cde-11db-9fe1-0800200c9a66', 
                       section.entry[0].substanceAdministration.id[0].root
  Assert.assertEquals 'cdbd5b03-6cde-11db-9fe1-0800200c9a66',
                       section.entry[1].substanceAdministration.id[0].root
  // ... the first medication has no start date
  Assert.assertEquals 0, section.entry[0].substanceAdministration.effectiveTime.size()
  Assert.assertEquals 1, section.entry[1].substanceAdministration.effectiveTime.size()

Parsing and Rendering of CDA documents

Parsing

You have two options for parsing CDA documents

  • Use an instance of org.openehealth.ipf.modules.cda.CDAR2Parser to parse a document into its internal CDA object model
  • As CDA documents are plain XML, use a native Groovy XML parser (e.g. XMLSlurper) to parse it into a hierarchy of generic Node objects.
Aspect CDAR2Parser XMLSlurper
processing speed slow fast
direct access using Groovy GPath syntax yes yes
usage of CDA model 'shortcuts' yes no
depthfirst/breadtfirst traversal in element tree no yes

This is an example on how to parse a CDA document from the file system using the CDAR2Parser


import org.openehealth.ipf.modules.cda.CDAR2Parser

InputStream is = getClass().getResourceAsStream("/SampleCDADocument.xml")
def clinicalDocument = new CDAR2Parser().parse(is)

Parsing using Groovy's XMLSlurper is equivalent:

InputStream is = getClass().getResourceAsStream("/SampleCDADocument.xml")
def clinicalDocument = new XMLSlurper().parse(is)

The next section explains for both cases, how you can access and extract data from the parsed document.

Extracting information from CDA documents

Extracting data from the parsed document differs a bit depending on whether CDAR2Parser or Groovy's XMLSlurper has been used for parsing. In the latter case, please also take a look at the corresponding Groovy documentation.

Whether to use CDAR2Parser or XMLSlurper
  • If you can abstain from using the CDA model extensions (in particular when working with CDA profiles like CCD), you should use XMLSlurper, because it's faster and offers depthFirst and breadthFirst traversal.
  • In you want to modify and re-render the document, or using the CDA model extensions is a must, use CDAR2Parser.

Depending on the parsing strategy, there are subtle differences on how to access the data:

Aspect CDAR2Parser XMLSlurper
usage of CDA model 'shortcuts' yes no
depthfirst/breadtfirst traversal in element tree no yes
accessing attributes element.attribute element.@attribute
accessing attribute/element content depends on data type xxx.text()

The following code snippets shows how to select and extract data from the sample CDA document contained in the CDA specification.

Parsed with CDAR2Parser

    InputStream is = getClass().getResourceAsStream(
            "/SampleCDADocument.xml");
    def clinicalDocument = new CDAR2Parser().parse(is);
    assertNotNull(clinicalDocument);
    def components = clinicalDocument.component.structuredBody.component

    // Simple navigation
    assertEquals('en-US', clinicalDocument.languageCode.code)
    assertEquals('KP00017', clinicalDocument.author[0].assignedAuthor.id[0].extension)

    // Avoid NullPointerException by with safe dereferencing using the ?. operator
    assertEquals('KP00017', clinicalDocument?.author[0].assignedAuthor.id[0].extension)
    def clinicalDocument2 = null
    assertNull(clinicalDocument2?.languageCode?.code)


    // Use any(Closure) to check if the predicate is true at least once
    assertTrue(components.any { it.section.code.code == '10164-2' })

    // Use every(Closure) to check if the predicate is always true
    assertTrue(components.every { it.section.code.codeSystem == '2.16.840.1.113883.6.1' })

    // Use find(Closure) to return the first value matching the closure condition    
    assertEquals('History of Present Illness',
            components.find { it.section.code.code == '10164-2' }.section.title.text)

    // Use findAll to return all values matching the closure condition
    assertEquals(1, components.findAll { it.section.code.code == '10164-2' }.size())

    // Use findIndexOf to return the index of the first item that matches the
    // condition specified in the closure.
    assertEquals(1, components.findIndexOf { it.section.code.code == '10153-2' })

    // Use collect to iterate through an object transforming each value into a
    // new value using the closure as a transformer, returning a list of transformed values. 
    assertEquals([
            'History of Present Illness',
            'Past Medical History',
            'Medications',
            'Allergies and Adverse Reactions',
            'Family history',
            'Social History',
            'Physical Examination',
            'Labs',
            'In-office Procedures',
            'Assessment',
            'Plan'],
            components.collect { it.section.title.text })

    // The spread operator parent*.action is equivalent to
    // parent.collect{ child -> child?.action }
    assertEquals([
            'History of Present Illness',
            'Past Medical History',
            'Medications',
            'Allergies and Adverse Reactions',
            'Family history',
            'Social History',
            'Physical Examination',
            'Labs',
            'In-office Procedures',
            'Assessment',
            'Plan'],
            components.section.title.text)

  }

Parsed with XMLSlurper

    InputStream is = getClass().getResourceAsStream(
            "/SampleCDADocument.xml");
    def clinicalDocument = new XmlSlurper().parse(is)

    def components = clinicalDocument.component.structuredBody.component

    // Simple navigation
    assertEquals('en-US', clinicalDocument.languageCode.@code.text())
    assertEquals('KP00017', clinicalDocument.author[0].assignedAuthor.id[0].@extension.text())

    // Avoid NullPointerException by with safe dereferencing using the ?. operator
    assertEquals('KP00017', clinicalDocument?.author[0].assignedAuthor.id[0].@extension.text())
    def clinicalDocument2 = null
    assertNull(clinicalDocument2?.languageCode?.@code?.text())


    // Use any(Closure) to check if the predicate is true at least once
    assertTrue(components.any { it.section.code.@code == '10164-2' })

    // Use every(Closure) to check if the predicate is always true
    assertTrue(components.every { it.section.code.@codeSystem == '2.16.840.1.113883.6.1' })

    // Use find(Closure) to return the first value matching the closure condition
    assertEquals('History of Present Illness',
            components.find { it.section.code.@code == '10164-2' }.section.title.text())

    // Use findAll to return all values matching the closure condition
    assertEquals(1, components.findAll { it.section.code.@code == '10164-2' }.size())

    // Use findIndexOf to return the index of the first item that matches
    // the condition specified in the closure.
    assertEquals(1, components.findIndexOf { it.section.code.@code == '10153-2' })

    // Use collect to iterate through an object transforming each value into a
    // new value using the closure as a transformer, returning a list of transformed values.
    assertEquals([
            'History of Present Illness',
            'Past Medical History',
            'Medications',
            'Allergies and Adverse Reactions',
            'Family history',
            'Social History',
            'Physical Examination',
            'Labs',
            'In-office Procedures',
            'Assessment',
            'Plan'],
            components.collect { it.section.title.text() })

    // The spread operator parent*.action is equivalent to
    // parent.collect{ child -> child?.action }
    assertEquals([
            'History of Present Illness',
            'Past Medical History',
            'Medications',
            'Allergies and Adverse Reactions',
            'Family history',
            'Social History',
            'Physical Examination',
            'Labs',
            'In-office Procedures',
            'Assessment',
            'Plan'],
            components.section.title*.text())

    // Use depthFirst (or '**') to search for elements anywhere in
    // the structure
    def drugCodes = clinicalDocument.depthFirst().findAll
      { it.name() == "manufacturedLabeledDrug" }.code*.@code
    
    assertEquals([
            '66493003',
            '91143003',
            '10312003',
            '376209006',
            '10312003',
            '331646005' ],
            drugCodes*.text())

    // Use of helper functions to encapsulate commonly used GPath expressions
    def drugCodes2 = findAllElements(clinicalDocument, "manufacturedLabeledDrug").code*.@code
    assertEquals(drugCodes, drugCodes2)
  }

  private Collection findAllElements(GPathResult result, String name) {
    return result.depthFirst().findAll { it.name() == name }
  }

Rendering

CDA documents created by either parsing or building their internal object representation can be easily rendered by using an instance of org.openehealth.ipf.modules.cda.CDAR2Renderer.


import org.openehealth.ipf.modules.cda.CDAR2Renderer

def document = builder.build {
   clinicalDocument {
      ...
   }
}

def renderer = new CDAR2Renderer()
def opts = [XMLResource.OPTION_DECLARE_XML : true,     // include XML declaration
            XMLResource.OPTION_ENCODING    : 'utf-8']  // encode as utf-8
println(renderer.render(document, opts))

The output looks like this:

<?xml version="1.0" encoding="utf-8"?>
<ClinicalDocument xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:hl7-org:v3" xsi:schemaLocation="urn:hl7-org:v3 CDA.xsd">
  <id extension="c266" root="2.16.840.1.113883.19.4"/>
  <code code="11488-4" codeSystem="2.16.840.1.113883.5.1" codeSystemName="LOINC" displayName="Consultation note"/>
  <title>Good Health Clinic Consultation Note</title>
  <effectiveTime value="20000407"/>
  ...
</ClinicalDocument>

Note that the renderer cared about adding proper namespaces to the XML document.

Validating CDA documents

CDA document instances in their XML representation can be validated using the W3C XML Schema and Schematron validators. The class org.openehealth.ipf.modules.cda.CDAR2Constants provides constants for the location of schema and schematron resources, e.g.

import org.openehealth.ipf.modules.cda.CDAR2Constants
import org.openehealth.ipf.commons.xml.XsdValidator
...
def validator = new XsdValidator()
validator.validate(xmldoc, CDAR2Constants.CDAR2_SCHEMA)
...

CDA Validation is nicely included into IPF's DSL extensions mechanism. For details refer to CDA DSL Extensions

Note that currently only XML-encoded CDA documents can be validated.
However, when creating a CDA document from scratch, the CDA Builder restricts this process by applying rules very close alongside the CDA MIF definition and the derived XML schema, even though you are working on an object structure rather than an XML document. So, there's a good chance that CDA documents created with the CDA Builder also pass the validators.

DSL extensions

This section describes DSL extensions provided by the platform-camel-cda component. The extensions allow to seemlessly integrate CDA parser, renderer, and validator into Camel/IPF integration routes.

CDA DSL extensions are defined in the org.openehealth.ipf.platform.camel.cda.extend.CDAModelExtension.groovy class. Their main purpose is to make CDA processing features available in Camel routes. Extensions provided by this class may well be combined with other extensions that comply with the DSL extension mechanism.

Configuration

For using the CDA DSL extensions you need to include the following dependency:

pom.xml
<dependency>
    <groupId>org.openehealth.ipf.platform-camel</groupId>
    <artifactId>platform-camel-cda</artifactId>
    <version>${ipf-version}</version>
</dependency>

This transitively includes the platform-camel-core and modules-cda libraries as well.

CDA (un)marshalling

The cdar2() DSL extension allows you to convert between CDA document strings (or streams) and org.openhealthtools.ihe.common.cdar2.POCDMT000040ClinicalDocument objects. For example, to unmarshal a CDA document from a string (or stream) use

Unmarshal adapter
    // ...
    from('...')
      .unmarshal().cdar2()
      .to('...')
      // ...

in your Groovy route definitions. As mentioned in the Parsing sections, CDA documents are plain XML, and if you do not require a semantic CDA model, you can also use Groovy's XMLSlurper, which reads the document into a hierarchy of Node objects. Please refer to DSL extensions for Groovy XML processing for more detail.