IPF Tutorials

Appendix B - IPF tutorials

The tutorials presented here cover the extensions that the Open eHealth IPF makes to Apache Camel. For an overview of these extensions refer to the overview section of the IPF reference manual. The focus of the IPF tutorials will be on writing integration solutions based on the IPF Scripting Layer which leverages the Groovy programming language. Although you may continue to write integration solutions in Java you'll miss many features for HL7 message processing that are available in Groovy only like the HAPI DSL, for example. Here's a list of tutorials for IPF.

Tutorial Description
First steps This tutorial is targeted at developers who want to get started with IPF but do not have a strong background on Camel and Groovy.
First details This tutorial is targeted at developers who want to get started with IPF and who are already familiar with Camel and Groovy.
HL7 processing Guides through some HL7 message processing examples.
OSGi tutorial Guides through the deployment of an IPF application on the IPF OSGi runtime.
Large binary support Guides through an LBS example.
Reference application Guides through the IPF reference application.
XDS demo repository Guides through the IPF XDS demo repository.

First steps tutorial

Prerequisites

Before you start working on this tutorial make sure that you've read the IPF development pages for setting up the development environment. For this tutorial it is not necessary to checkout the IPF sources, all IPF dependencies are downloaded from the Open eHealth Maven repository.

This tutorial is targeted at developers who want to get started with IPF and do not have a strong background on Camel and Groovy. The main goal is to have a very simple IPF application up and running very quickly. More technical background information is given in the First details tutorial.

  • We start by creating a project from an IPF archetype
  • We see how integration logic looks like in IPF, using the IPF Scripting Layer
  • We then extend integration logic in order to
    • receive a message over HTTP
    • transform that message
    • write the result to a file.
  • The messaging solution will be tested both automated and manually.
  • The messaging solution will be packaged, installed and started.

Source code

The source code for this tutorial can be downloaded from here.

Project creation

We start by creating an IPF project via a Maven archetype. Navigate to a directory of your choice and create the project by entering the following on the command line (make sure to have the command on a single line):

mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate 
-DarchetypeGroupId=org.openehealth.ipf.archetypes 
-DarchetypeArtifactId=ipf-archetype-basic 
-DarchetypeVersion=1.7.0 
-DgroupId=org.openehealth.tutorial 
-DartifactId=basic 
-Dversion=1.0-SNAPSHOT 
-DinteractiveMode=false

This will create a folder named basic in the current directory. Change to the basic folder and enter

mvn install

This will compile the project and install the project artifacts into your local Maven cache. To import the project into Eclipse navigate to File->Import->General->Existing Projects into Workspace in the Eclipse menu and select the created basic folder as root directory. After having imported the project into Eclipse it should look similar to the following figure (exact display might vary depending on your workspace settings).

Clean project after import

Eclipse might report compile errors for the imported project. In this case it is sufficient to clean the org.openehealth.tutorial.basic project. Select the project and then select Project->Clean ... from the main menu. In the dialog select Clean projects selected below (make sure that the org.openehealth.tutorial.basic project is actually selected) and press the OK button.

In addition to an IPF application skeleton the archetype also created some simple example files that can be used for initial experiments.
We'll mainly work with org.openehealth.tutorial.SampleRouteConfig.groovy in the src/main/resources path, which implements a trivial piece of message processing.

Route definition

SampleRouteBuilder.groovy defines two routes. A route is a logical unit of a sequence of message processing steps. The routes are implemented in the Groovy programming language, but we'll see that the syntax used is very close to Java.

SampleRouteBuilder.groovy
class SampleRouteBuilder extends SpringRouteBuilder {
    void configure() {
        from('direct:input1').transmogrify { it * 2 } // duplicate the request string e.g. 'abc' -> 'abcabc'
        from('direct:input2').reverse()               // revert the request string e.g. 'abc' -> 'cba'
    }
}
  1. The first route receives a message from a direct:input1 endpoint, and transmogrifies (i.e. convert to something different) the message by duplicating it. The duplication is defined by a Groovy Closure, an elegant way to define a simple code block that is executed when a message arrives. The it-variable is the Groovy default parameter to the code block, which in our case contains the message we're processing. In Groovy you can use the * (multiply) operator on a string for repeating that string.
  2. The second route that receives a message from a direct:input2 endpoint, reverses the input message body and returns the result. The reverse() command is a tutorial-specific extension defined in SampleModelExtension.groovy and is just a shortcut notation for
.transmogrify { it.reverse() }

For the moment we can treat direct endpoints as internal labels of the Camel routes, that can e.g. be used from JUnit tests like the following:

SampleRouteTest.java
...
@ContextConfiguration(locations = { "/context.xml" })
public class SampleRouteTest {

    @Autowired
    private ProducerTemplate<Exchange> producerTemplate;
    ....
    
    @Test
    public void testMultiply() throws Exception {
        assertEquals("abcabc", producerTemplate.requestBody("direct:input1", "abc"));
    }
    
    @Test
    public void testReverse() throws Exception {
        assertEquals("cba", producerTemplate.requestBody("direct:input2", "abc"));
    }
    

This test sends a message to each endpoint and expects the messages to be duplicated or reversed, respectively.

Route extension

We now extend the example to make our integration logic accessible from the outside.

  • We'll receive an HTTP request message from a jetty endpoint,
  • transform it and
  • send it to a file endpoint.

In addition we want to keep the direct endpoints so that we can still test the message transformation with the JUnit test shown above. Instead of only returning the processing result to the HTTP client we also write the message to a file. Here's the extended route definition:

SampleRouteBuilder.groovy
class SampleRouteBuilder extends SpringRouteBuilder {
    
    void configure() {

        from('jetty:http://0.0.0.0:8080/tutorial')     // receive client requests on http://0.0.0.0:8080/tutorial
            .convertBodyTo(String.class)               // convert request input stream into a string
            .to('direct:input1')                       // continue from direct:input1

        from('direct:input1')
            .transmogrify { it * 2 }                   // duplicate the request string
            .setFileHeaderFrom('destination')          // set name of result file to be written (a custom DSL extension)
            .to('file:target/output?append=false')     // replace content of file in target/output directory with body of in-message.

        from('direct:input2').reverse()
    }
}
  • To receive messages over HTTP we have to create an HTTP endpoint, which will run a Jetty HTTP server listening on port 8080. The endpoint doesn't automatically convert the HTTP body from an InputStream to a String so we have to do that manually via convertBodyTo(String.class) as we expect a String body in later processing steps.
  • After converting the message, we tell the file component how to name the output file. This is done via setFileHeaderFrom(java.lang.String). In this example the file name is taken from the message header 'destination'. setFileHeaderFrom is a custom extension defined by this tutorial and must be added to the SampleModelExtension.groovy file (see next box). You don't have to understand the exact details of this extension mechanism. For now it is sufficient to see that the route definition can also contain custom elements.
  • The file endpoint finally writes the results to the target/output directory.
SampleModelExtension.groovy
class SampleModelExtension {
     static extensions = {
         ...
         ProcessorType.metaClass.setFileHeaderFrom = { String sourceHeader ->  
             delegate.setHeader('org.apache.camel.file.name') { exchange -> 
                 def destination = exchange.in.headers."$sourceHeader"
                 destination ? "${destination}.txt" : 'default.txt'
             }
         }
     }
}

Before we can start the route we additionally have to define a dependency to the camel-jetty component in our pom.xml file.

pom.xml
<project>

    <dependencies>
        ...
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-jetty</artifactId>
            <version>1.6.0</version>
        </dependency>
        ...
    </dependencies>

<project>

Project testing

Unit test

When we run the unit test SampleRouteTest.java we should see a file default.txt in the target/output folder, because we didn't set a 'destination' message header. The unit test sent the message body abc so there should be a repeated abcabc contained in the output file.

You run the test inside Eclipse or using Maven by entering

mvn test

on the command line in the basic folder.

Server test

For testing the HTTP endpoint we first have to start the server. To do so, right-click on SampleServer.java and select Run As->Java Application. This will start a standalone route or integration server that listens on port 8080 for requests. As HTTP client we use the Eclipse HTTP Client. To prepare the test open the Eclipse HTTP client and

  • Enter http://localhost:8080/tutorial in the address field (top-left corner of the window)
  • Select POST in the HTTP method field (top-right corner of the window)
  • Enter destination=custom in the Headers field
  • Enter test in the Body field

To submit the request press the green arrow at the top of the window. The result should look like:

The HTTP endpoint copies all HTTP headers onto Camel message headers, so we have the destination header present for creating a custom file name. In our example the destination header value is custom and the output file therefore has the name custom.txt. You should now see this file as well as the file generated during the unit test default.txt in the target/output folder. The content of the custom.txt file should be testtest because we sent a test HTTP request body.

Assembly and installation

We finally want to create a distribution package from our project, install (unzip) that package somewhere and start a standalone integration server (i.e. an integration server that runs outside Eclipse). Before continuing make sure that you stopped the SampleServer that you started within Eclipse, otherwise, you won't be able to start another server. To create the package enter

mvn assembly:assembly

on the command line. The created package basic-1.0-SNAPSHOT-bin.zip is written to the project's target folder. Copy the package to a new location and unzip it. This will create a folder named basic-1.0-SNAPSHOT with the following content:

Start server

To start the server run startup.bat. The console output should like like

Finally, submit the HTTP request again that you've configured before with the Eclipse HTTP client. This should again create a file custom.txt in the newly created target/output folder.

Summary

In this tutorial we've seen

  • how to create an integration project skeleton by using Maven archetypes
  • how message processing routes are defined
  • how message processing routes are tested
  • how easy it is to add HTTP or File endpoints as external interfaces
  • how an integration project is bundled and launched as a standalone application
Continue with XML processing on IPF

After this tutorial you might want to take a look at IPF's XML processing and transformation capabilities which are based on Groovy's XML support.

First details tutorial

Prerequisites

Before you start working on this tutorial make sure that you've read the IPF development pages for setting up the development environment. For this tutorial it is not necessary to checkout the IPF sources, all IPF dependencies are downloaded from the Open eHealth Maven repository.

This tutorial is targeted at developers who want to get started with IPF and who are already familiar with Camel and Groovy. It goes into some technical details for explaining how IPF applications internally work. If this is your first contact with IPF, we recommend working through the First steps tutorial first, which omits most of the technical explanations.

  • We start by creating a project from an IPF archetype
  • The created project structure is explained in detail. You'll see
  • We then write a very simple messaging solution that
    • receives a message over HTTP
    • transforms that message
    • writes the result to a file.
  • The messaging solution will be tested both automated and manually.
  • The messaging solution will be packaged, installed and started.

Source code

The source code for this tutorial can be downloaded from here.

Project creation

We start by creating an example IPF project by entering

mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate 
-DarchetypeGroupId=org.openehealth.ipf.archetypes 
-DarchetypeArtifactId=ipf-archetype-basic 
-DarchetypeVersion=1.7.0 
-DgroupId=org.openehealth.tutorial 
-DartifactId=basic 
-Dversion=1.0-SNAPSHOT 
-DinteractiveMode=false

on the command line (make sure to have the command on a single line). This will create a folder named basic in the current directory. Change to the basic folder an enter

mvn install

This will compile the project and install the project artifacts into your local Maven cache. To import the project into Eclipse navigate to File->Import->Existing Projects into Workspace in the Eclipse menu and select the created basic folder as root directory. Refer to the archetype section of the IPF Development page for further details on the created project structure. After having imported the project into Eclipse it should look like in the following figure.

Clean project after import

Eclipse might report compile errors for the imported project. In this case it is sufficient to clean the org.openehealth.tutorial.basic project. Select the project and then select Project->Clean ... from the main menu. In the dialog select Clean projects selected below (make sure that the org.openehealth.tutorial.basic project is actually selected) and press the OK button.

In addition to an IPF application skeleton the archetype also created some simple example files that can be used for initial experiments. Here's an overview of the created project files.

File Package Path Description
SampleRouteBuilder.groovy org.openehealth.tutorial src/main/groovy Implements the message processing route.
SampleModelExtension.groovy org.openehealth.tutorial src/main/groovy Implements tutorial-specific DSL extensions used within SampleRouteBuilder.groovy.
SampleRouteTest.java org.openehealth.tutorial src/test/java A JUnit test for the route implemented by SampleRouteConfig.groovy.
SampleServer.java org.openehealth.tutorial src/main/java A class for starting the route as standalone server.
context.xml - src/main/resources The Spring application context wiring the individual platform and application components.
bin.xml - src/main/assembly The Maven 2 assembly descriptor for creating a distributable assembly zip file of the tutorial application.
pom.xml - . The Maven 2 project descriptor of the tutorial application.
startup.bat - . The Windows startup file for a standalone server. This startup file can only be used in an installation of a distributable package. It cannot be used directly inside the original Eclipse/Maven project.
startup.sh - . The Linux startup file for a standalone server. This startup file can only be used in an installation of a distributable package. It cannot be used directly inside the original Eclipse/Maven project. This file is currently empty.
.project - . The project descriptor of the sample Eclipse project.
.classpath - . The classpath definition of the sample Eclipse project.

Route definition

SampleRouteBuilder.groovy defines two routes.

SampleRouteBuilder.groovy
class SampleRouteBuilder extends SpringRouteBuilder {
    void configure() {
        from('direct:input1').transmogrify { it * 2 } // duplicate the request string e.g. 'abc' -> 'abcabc'
        from('direct:input2').reverse()               // revert the request string e.g. 'abc' -> 'cba'
    }
}
  1. A route that receives a message from a direct:input1 endpoint, multiplies the body of the input message by 2 and returns the result. In Groovy you can use the * (multiply) operator on a string for repeating that string. For transformation we use the transmogrify DSL extension provided by IPF and implement the transformation logic with a closure. Input to the closure is by default the body of the in-message. The result returned by the closure is set to the body of the result message. For a detailed description of the transmogrify DSL element refer to the DSL extensions for IPF module adapters section of the reference manual.
  2. A route that receives a message from a direct:input2 endpoint, reverses the input message body and returns the result. The reverse() DSL element is not provide by Camel or IPF, it is a tutorial-specific DSL extension defined in SampleModelExtension.groovy. This extension uses Groovy GDK's java.lang.String.reverse() method to reverse the character sequence of the input message string. For a detailed description of the DSL extension mechanism refer to the DSL extension mechanism section of the reference manual.

We use direct endpoints to access the Camel routes from JUnit tests. Behind the scenes communication with these endpoints is via Java method calls. The unit tests, as generated by the archetype, communicate with the route via a two-way (in-out), synchronous communication pattern. The corresponding in-out message exchange is implicitly created by the requestBody() method of the injected producerTemplate (an instance of type org.apache.camel.ProducerTemplate).

SampleRouteTest.java
...
@ContextConfiguration(locations = { "/context.xml" })
public class SampleRouteTest {

    @Autowired
    private ProducerTemplate<Exchange> producerTemplate;

    ....
    
    @Test
    public void testMultiply() throws Exception {
        assertEquals("abcabc", producerTemplate.requestBody("direct:input1", "abc"));
    }
    
    @Test
    public void testReverse() throws Exception {
        assertEquals("cba", producerTemplate.requestBody("direct:input2", "abc"));
    }
    

Using Enterprise Integration Pattern symbols this looks as follows:

Later we'll see how to route the message processing results to a final destination in addition to routing it back to the sender endpoint. The final destination will be a file endpoint that writes the message body to a file.

Extension definition

The second route makes use of the reverse() method, a custom-defined DSL extension *). Methods that make up the DSL are defined on route builders and model classes. Model classes are contained in the packages

  • org.apache.camel.model
  • org.openehealth.ipf.platform.camel.core.model and
  • org.openehealth.ipf.platform.camel.flow.model

Because Camel doesn't provide a mechanism to extend the DSL on Java-level we use Groovy meta-programming mechanisms to enhance existing model classes where needed (or even define our own model classes like IPF does). In our example, we enhance the org.apache.camel.model.ProcessorType class with the reverse method.

SampleModelExtension.groovy
class SampleModelExtension {

     static extensions = {
         
         ProcessorType.metaClass.reverse = {
             delegate.transmogrify { it.reverse() }
         }
         
     }
     
}

We obtain the ProcessorType 's metaClass property and use it to inject a new method definition, reverse in our example. The method implementation is given by a closure that makes use of the transmogrify extension, another extension provided by IPF. The ProcessorType instance on which the extension method gets called is available as delegate variable inside the method implementation closure. Argument to transmogrify is another closure that finally applies Groovy's java.lang.String.reverse() method on the message body (it). In our example we expect the message body to be a String. Using these simple mechanisms we extended the Camel DSL. However, you should also be aware of the limitations of the DSL extension mechanism.

Extensions introduced by IPF applications should be defined inside an extensions block in any concrete class you like. Then these extensions can be activated by configuring a route model extender in the Spring application context (see context.xml later).

*) For a complete reference of predefined DSL extensions provided by IPF refer to the IPF extensions index.

Application configuration

The following XML file shows a Spring configuration of an application that makes use of IPF's core features only. For examples how to configure support for flow management and HL7 message processing refer to the flow management tutorial and HL7 processing tutorial, respectivly.

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

    <camel:camelContext id="camelContext" /> 

    <bean id="producerTemplate" 
        factory-bean="camelContext"
        factory-method="createProducerTemplate">
    </bean>

    <bean id="routeBuilder" depends-on="routeModelExtender"
        class="org.openehealth.tutorial.SampleRouteBuilder">
    </bean>

    <bean id="sampleModelExtension" 
        class="org.openehealth.tutorial.SampleModelExtension">
    </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="sampleModelExtension" />
            </list>
        </property>
    </bean>

</beans>

The following table provides further information about the beans configured in the above application context.xml file.

Bean Description
camelContext The Camel context. It represents a single Camel routing rulebase.
producerTemplate The producer template injected into JUnit test classes. It is used to send messages to endpoints.
routeBuilder The sample route builder. This bean depends on the routeModelExtender to ensure that the DSL extensions are activated before the route definitions are processed.
coreModelExtension The DSL extensions provided by the platform-camel-core component.
sampleModelExtension The sample DSL extensions.
routeModelExtender The extender object that calls the extensions blocks in the *ModelExtension beans during application initialization.

Project descriptor

Here is an excerpt of the Maven 2 project descriptor.

pom.xml
<project>
    
    ...
    <groupId>org.openehealth.tutorial</groupId>
    <artifactId>basic</artifactId>
    <version>1.0-SNAPSHOT</version>
    ...

    <dependencies>
        <dependency>
            <groupId>org.openehealth.ipf.platform-camel</groupId>
            <artifactId>platform-camel-core</artifactId>
            <version>1.7.0</version>
        </dependency>
        ...
    </dependencies>

    <build>
        <plugins>
            ...
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptors>
                        <descriptor>src/main/assembly/bin.xml</descriptor>
                    </descriptors>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>

The relevant entries are:

  • <groupId> (defined at project-creation time).
  • <artifactId> (defined at project-creation time).
  • <version> (defined at project-creation time).
  • Dependency to platform-camel-core. That's the only IPF dependency needed for simple IPF applications.
  • Reference to the bin.xml assembly descriptor in the maven-assembly-plugin for creating binary distribution files.

Assembly descriptor

The assembly descriptor describes how to package the project into a distributable package.

bin.xml
<assembly>
    <id>bin</id>
    <formats>
        <format>zip</format>
    </formats>
    <fileSets>
        <fileSet>
            <includes>
                <include>startup*</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>src/test/resources</directory>
            <outputDirectory>conf</outputDirectory>
            <includes>
                <include>log4j.xml</include>
            </includes>
        </fileSet>
    </fileSets>
    <dependencySets>
        <dependencySet>
            <outputDirectory>lib</outputDirectory>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

In our example we define the package format (zip) and specify the filesets to be included into the package. We also define a <dependencySet> which causes the maven-assembly-plugin to include all runtime dependencies into the package under the lib directory. For further configuration details consult the Maven assembly plugin documentation. Section assembly and installation will explain how to create the distribution package.

Project customization

In this section we'll extend the example to receive an HTTP request message from a jetty endpoint, transform it and write send it to a file endpoint. Using Enterprise Integration Pattern symbols this looks as follows:

We want to keep the direct endpoint so that we can still test the message transformation with a JUnit test. To reuse that endpoint the additional jetty endpoint communicates with the transformer over the direct endpoint. Instead of only returning the processing result to the initiators (HTTP client or JUnit test) we also write the message to a final destination which is a file endpoint. This endpoint creates files containing the processing result. Here's the extended route definition:

SampleRouteBuilder.groovy
class SampleRouteBuilder extends SpringRouteBuilder {
    
    void configure() {

        from('jetty:http://0.0.0.0:8080/tutorial')     // receive client requests on http://0.0.0.0:8080/tutorial
            .convertBodyTo(String.class)               // convert request input stream into a string
            .to('direct:input1')                       // continue from direct:input1

        from('direct:input1')
            .transmogrify { it * 2 }                   // duplicate the request string
            .setFileHeaderFrom('destination')          // set name of result file to be written (a custom DSL extension)
            .to('file:target/output?append=false')     // replace content of file in target/output directory with body of in-message.

        from('direct:input2').reverse()

    }
}

In this example we only extended the first of the two routes that have been initially created. We leave the second route untouched. To receive messages over HTTP we have to create a jetty endpoint via the jetty:http://localhost:8080/tutorial. This will run a Jetty server listening on port 8080. The context path is tutorial. The jetty endpoint doesn't automatically convert the HTTP body from an InputStream to a String so we have to do that manually via convertBodyTo(String.class) (we expect a string body in later processing steps). Then the message received via HTTP is forwarded to the direct:input1 endpoint, the same that is used in our JUnit test. We again transform the original message body by repeating it (multiply it by 2) using transmogrify { it * 2 }. In the next step we tell the file component that the filename is determined via the message header destination. This is done with setFileHeaderFrom(java.lang.String). This is a custom DSL extension that is implemented as follows:

SampleModelExtension.groovy
class SampleModelExtension {
     static extensions = {
         ...
         ProcessorType.metaClass.setFileHeaderFrom = { String sourceHeader ->  
             delegate.setHeader('org.apache.camel.file.name') { exchange -> 
                 def destination = exchange.in.headers."$sourceHeader"
                 destination ? "${destination}.txt" : 'default.txt'
             }
         }
     }
}

We define a new method setFileHeaderFrom on the ProcessorType class. This makes setFileHeaderFrom available as a new DSL element in route definitions. This method derives the value of the org.apache.camel.file.name header from the value of another user-defined header. The org.apache.camel.file.name header is understood by the file component. This way we can influence the name of the file being written. The user-defined header is the argument to the setFileHeader extension method (sourceHeader parameter).

In our example, we derive the destination file name from the destination message header. The file endpoint defined in SampleRouteConfig.groovy finally writes the results to the target/output directory. Before we can start the route we have to define a dependency to the camel-jetty component in our pom.xml file.

pom.xml
<project>

    <dependencies>
        ...
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-jetty</artifactId>
            <version>1.6.0</version>
        </dependency>
        ...
    </dependencies>

<project>

Project testing

Unit test

After running the unit tests we find a file default.txt in the target/output folder. The file default.txt was generated because we didn't set a destination header in the unit tests. The unit test sent the message body abc so there should be a repeated abcabc contained in the output file.

Server test

For testing the HTTP endpoint we first have to start the server. To do so, right-click on SampleServer.java and select Run As->Java Application. This will start a standalone route or integration server that listens on port 8080 for requests. As HTTP client we use the Eclipse HTTP Client. To prepare the test open the Eclipse HTTP client and

  • Enter http://localhost:8080/tutorial in the address field (top-left corner of the window)
  • Select POST in the HTTP method field (top-right corner of the window)
  • Enter destination=custom in the Headers field
  • Enter test in the Body field

To submit the request press the green arrow at the top of the window. The result should look like:

The jetty endpoint copies all HTTP headers onto Camel message headers, so we have the destination header present for creating a custom file name. In our example the destination header value is custom and the output file therefore has the name custom.txt. You should now see this file as well as the file generated during the unit test default.txt in the target/output folder. The content of the custom.txt file should be testtest because we sent a test HTTP request body.

Assembly and installation

We finally want to create a distribution package from our project, install (unzip) that package somewhere and start a standalone integration server (i.e. an integration server that runs outside Eclipse). Before continuing make sure that you stopped the SampleServer that you started within Eclipse, otherwise, you won't be able to start another server. To create the package enter

mvn assembly:assembly

on the command line. The created package basic-1.0-SNAPSHOT-bin.zip is written to the project's target folder. Copy the package to a new location and unzip it. This will create a folder named basic-1.0-SNAPSHOT with the following content:

The lib folder contains the project jar file (basic-1.0-SNAPSHOT.jar) as well as all required runtime dependencies. The conf folder contains a log4j.xml configuration file. Startup scripts are located directly under the root folder. How the project is packaged can of course be customized by changing the assembly descriptor of the project in src/main/assembly/bin.xml. The script startup.sh is currently empty i.e. for testing-purposes you have to run the server on Windows using startup.bat.

Start server

To start the server run startup.bat. The console output should like like

Finally, submit the HTTP request again that you've configured before with the Eclipse HTTP client. This should again create a file custom.txt in the newly created target/output folder.

Continue with XML processing on IPF

After this tutorial you might want to take a look at IPF's XML processing and transformation capabilities which are based on Groovy's XML support.

HL7 processing tutorial

Prerequisites

This tutorial guides you through an HL7 version 2 message processing example. Message processing is done via IPF's HL7 DSL and via HL7-specific extensions to the Camel DSL. We will create an IPF application that reads an HL7 version 2 message from an HTTP endpoint, validates and transforms the message, and writes the transformation result to an output file.

HL7 features of Camel and IPF

This tutorial currently doesn't use Camel's HL7 features. Camel's HL7 features and those provided by IPF are complementary to each other and can perfectly be combined. For example to receive HL7 version 2 messages via MLLP you can do so using Camel's HL7 component.

Validation

Let's look at the message we will use in our example. The message is an ADT A01 message, version 2.2, and we want to enforce that

  • all primitive type values have the correct format and
  • the message itself contains a defined sequence of segments

The HL7 module of IPF comes with an HL7 validation DSL that makes the definition of validation rules very easy. Even better, IPF provides a complete set of rules that check whether the primitive type values meet the constraints defined in the HL7 specification.

Transformation

Now lets look the transformation we want to perform if we passed validation.

  • The room number and bed number from the PV1[3] field shall be dropped. This field is a composite field and we want to drop the second and the third component.
  • The birth date in PID[7] shall be reformatted by dropping the last 6 digits. This field is also a composite field with only a single component.
  • The gender code in PID[8] shall be mapped to a code from another code system. We will use a simple code mapping service for that.

Finally, we will derive the destination file name from the MSH[4] field (sending facility).

Route design

Here's the message processing route using enterprise integration pattern symbols.

  • An ADT A01 event message is received via a jetty endpoint (inbound HTTP endpoint).
  • The message is validated as described above.
  • The message is forwarded to a transformer that makes the changes to the HL7 message as described above.
  • The transformation result is placed into different files whose names are derived from the MSH[4] field.

Source code

The source code for this tutorial can be downloaded from here.

Project creation

We start by creating an example IPF project by entering

mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate
-DarchetypeGroupId=org.openehealth.ipf.archetypes
-DarchetypeArtifactId=ipf-archetype-basic
-DarchetypeVersion=1.7.0
-DgroupId=org.openehealth.tutorial
-DartifactId=hl7
-Dversion=1.0-SNAPSHOT
-DinteractiveMode=false

on the command line (make sure to have the command on a single line). This will create a folder named hl7 in the current directory. Change to the hl7 folder an enter

mvn install

This will compile the project and install the project artifacts into your local Maven cache. To import the project into Eclipse navigate to File->Import->Existing Projects into Workspace in the Eclipse menu and select the created hl7 folder as root directory. After having imported the project into Eclipse it should look like in the following figure.

You can find a detailed description of the created project structure in the project creation section of the first details tutorial.

Clean project after import

Eclipse might report compile errors for the imported project. In this case it is sufficient to clean the org.openehealth.tutorial.hl7 project. Select the project and then select Project->Clean ... from the main menu. In the dialog select Clean projects selected below (make sure that the org.openehealth.tutorial.hl7 project is actually selected) and press the OK button.

Extend project descriptor

In order to enable HL7 processing and communication over HTTP we have to include further dependencies into the Maven project descriptor pom.xml. These are

Dependency Description
platform-component-hl7 IPF component that provides HL7-specific extensions to the Camel DSL.
modules-hl7dsl IPF component that provides the HL7 DSL and is transitively included via platform-camel-hl7.
modules-hl7 IPF component that provides the HAPI extensions and is transitively included via platform-camel-hl7.
camel-jetty Camel component that enables inbound communication over HTTP.

The pom.xml file is located directly under the project's root directory. Here's an excerpt of the extended project descriptor.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.openehealth.tutorial</groupId>
    <artifactId>hl7</artifactId>
    <name>hl7</name>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.openehealth.ipf.platform-camel</groupId>
            <artifactId>platform-camel-hl7</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-jetty</artifactId>
            <version>1.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>2.5.6</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        ...
    </build>

    ...

</project>

Extend application context

The components we included in the previous section must also be configured in the Spring application context.xml file. This file is located under src/main/resources. In addition to the default configuration created by the archetype we need to

  • Register the HL7 DSL extensions provided by the platform-camel-hl7 component at the model extender
  • Register the HAPI extensions provided by the modules-hl7 component at the model extender
  • Configure a mapping service that we will use for code mappings
  • Add a couple of beans needed for HL7 validation

Here's the complete application context.xml file.

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

    <camel:camelContext id="camelContext" />

    <bean id="producerTemplate"
        factory-bean="camelContext"
        factory-method="createProducerTemplate">
    </bean>

    <bean id="routeBuilder" depends-on="routeModelExtender"
        class="org.openehealth.tutorial.SampleRouteBuilder"/>

    <!-- Code mapping service using map.groovy as mapping table -->
    <bean id="mappingService"
        class="org.openehealth.ipf.modules.hl7.mappings.BidiMappingService">
        <property name="mappingScript" value="classpath:map.groovy"/>
    </bean>

    <!-- DSL extensions provided by platform-camel-core -->
    <bean id="coreModelExtension"
        class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension">
    </bean>

    <!-- DSL extensions provided by platform-camel-hl7 -->
    <bean id="hl7ModelExtension"
        class="org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension"/>

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

    <bean id="routeModelExtender"
        class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender">
        <property name="routeModelExtensions">
            <list>
                <ref bean="coreModelExtension" />  <!-- Register core DSL extensions -->
                <ref bean="hl7ModelExtension" />   <!-- Register HL7 DSL extensions -->
                <ref bean="hapiModelExtension" />  <!-- Register HAPI extensions -->
            </list>
        </property>
    </bean>

   <!-- All we need for validation: a validator, a validation context, -->
   <!-- and two set of rules                                           -->
   <bean id="myValidatorBean" class="org.openehealth.ipf.modules.hl7.validation.support.HL7Validator"/>

   <bean id="validationContext"
       class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/>

   <bean id="defaultTypeRules"
       class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/>

   <bean id="myCustomRules"
       class="org.openehealth.tutorial.SampleRulesBuilder"/>

</beans>

The mapping table map.groovy referenced by the mappingService bean is described in the code mapping section.

Route definition

Lets start with an overview of the complete route and then discuss it step by step. Before doing so open the SampleRouteBuilder.groovy file in Eclipse, add a validationContext property and replace the existing routes in the configure() method.

'SampleRouteBuilder.groovy'
package org.openehealth.tutorial

import org.apache.camel.spring.SpringRouteBuilder
import ca.uhn.hl7v2.validation.ValidationContext

class SampleRouteBuilder extends SpringRouteBuilder {

    void configure() {
        from('jetty:http://localhost:8080/tutorial')                  // start HTTP server
            .to('direct:input')                                       // forward request

        from('direct:input')                                          // receive HL7 message
            .unmarshal().ghl7()                                       // create message adapter (HL7 DSL support)
            .validate().ghl7().profile(bean(ValidationContext.class)) // validate against custom validation context
            .transmogrify { msg ->
                msg.PV1[3][2] = ''                                    // clear room nr.
                msg.PV1[3][3] = ''                                    // clear bed nr.
                msg.PID[7][1] = msg.PID[7][1].value.substring(0, 8)   // format birth date
                msg.PID[8]    = msg.PID[8].mapGender()                // map 'gender' code
                msg                                                   // return result
            }
            .setHeader('org.apache.camel.file.name') {exchange ->     // set filename header to
                exchange.in.body.MSH[4].value + '.hl7'                // sending facility (MSH[4])
            }
            .marshal().ghl7()                                         // convert to external representation
            .to('file:target/output?append=false')                    // write external representation to file
    }    
}

First, we define a jetty endpoint for receiving HL7 messages over HTTP.

...
from('jetty:http://localhost:8080/tutorial')                  // start HTTP server
    .to('direct:input')                                       // forward request
...

This will start an HTTP server listening on port 8080. The context path is tutorial. The message is then forwarded to a direct endpoint that we also use inside our JUnit tests for sending test messages. HL7 messages arrive as InputStream at the direct:input endpoint . We won't work on this representation of the HL7 message. Instead we create a so-called message adapter that implements the HL7 DSL. Creation of a message adapter from an InputStream is done via unmarshal().ghl7() (this also works if the message is a String).

...
from('direct:input')                                          // receive HL7 message
     .unmarshal().ghl7()                                      // create message adapter (HL7 DSL support)
...

The next step in the HL7 message validation using the HL7 Validation framework.

...
.validate().ghl7().profile(validationContext)
...

The validation is executed against the HAPI message object. The profile DSL extension initializes the validator with the HL7 ValidationContext that has been injected into the route builder.

Now, we add our custom SampleRulesBuilder.groovy script, that defines the allowed sequence and cardinalities of HL7 groups and segments for this message. The file is added to the src/main/groovy directory in the package org.openehealth.tutorial:

SampleRulesBuilder.groovy
package org.openehealth.tutorial

import org.openehealth.ipf.modules.hl7.validation.builder.RuleBuilder
import org.openehealth.ipf.modules.hl7.validation.builder.ValidationContextBuilder
import ca.uhn.hl7v2.validation.ValidationContext


class SampleRulesBuilder extends ValidationContextBuilder{

    // We define only a subset of the segments defined in the HL7 2.2 spec

    RuleBuilder forContext(ValidationContext context) {
        new RuleBuilder(context)
          .forVersion('2.2')
            .message('ADT', 'A01').abstractSyntax(
                    'MSH',
                    'EVN',
                    'PID',
                    [  {  'NK1'  }  ],
                    'PV1',
                    [  {  INSURANCE(
                              'IN1',
                              [  'IN2'  ] ,
                              [  'IN3'  ]
                    )}]
            )
    }

}

The sequence and cardinality of groups and segments is defined in a syntax that is very closely related to the HL7 Abstract Message Syntax. The message is required to contain a MSH, EVN, PID and PV1 segment; it may contain any number of NK1 segments and it may contain a repeatable INSURANCE group. For detail, please read the documentation on HL7 validation.
As our test message matches this definition, we expect the message to pass validation.

The next step in the route is the HL7 message transformation using the HL7 DSL.

...
.transmogrify { msg ->
    msg.PV1[3][2] = ''                                    // clear room nr.
    msg.PV1[3][3] = ''                                    // clear bed nr.
    msg.PID[7][1] = msg.PID[7][1].value.substring(0, 8)   // format birth date
    msg.PID[8]    = msg.PID[8].mapGender()                // map 'gender' code
    msg                                                   // return result
...
}

We do the transformation with a transmogrifier closure. Inside the closure we use the HL7 DSL. The HL7 DSL allows us to manipulate HL7 messages on a very high level without dealing with low level HL7 API details. You immediately see how the message is processed by looking at the code. For accessing a message segment you directly use the segment name like msg.PV1 or msg.PID. Fields and sub-fields (components of a composite field) are accessed by indices starting from 1. For example, PV1[3][2] denotes the second component of the third PV1 field. For a complete reference of the HL7 DSL refer to the HL7 DSL section of the reference manual. Code mapping is done with the mapGender() method. This method translates the English F code (female) contained in PID[8] to a German W code (weiblich). This will be explained in more detail in the code mapping section.

The transformation result can now be written to a file. We derive the name of the file from the content of the MSH[4] field. For example, from HZL we write a file named HZL.hl7, from PKL a file named PKL.hl7 ... and so on. We achieve this by setting the org.apache.camel.file.name message header which is understood by the file component.

...
.setHeader('org.apache.camel.file.name') {exchange ->     // set filename header to
    exchange.in.body.MSH[4].value + '.hl7'                // sending facility (MSH[4])
}
.marshal().ghl7()                                         // convert to external representation
.to('file:target/output?append=false')                    // write external representation to file
...

For setting the message header we use the setHeader() method. Inside the setHeader closure we again use the HL7 DSL to access the MSH[4] field. Before the message can actually be written to a file we have to marshal the message adapter with marshal().ghl7(). This is the reverse operation to unmarshal().ghl7() at the beginning of the route. The file endpoint is configured in a way that it writes files to the target/output directory. Writing to an existing file will overwrite its content (append=false). Before we can start testing the route we have to setup the code mapping table.

Code mapping

In the extend application context section we've configured the code mapping service to use a mappingScript named map.groovy. In our example, we only need a single gender mapping table with a single entry that maps F to W.

map.groovy
mappings = {
  gender(
    F      : 'W',

    (ELSE) : { it }
  )
}

This format is valid Groovy syntax and is understood by the default bi-directional mapping service provided by IPF. For a complete reference refer to the mapping service section of the reference manual. You may also provide your own mapping service implementation by implementing the org.openehealth.ipf.modules.hl7.mappings.MappingService interface. To install the mapping table create a map.groovy file under src/main/resources and copy the above mappings block into that file.

It is interesting to see how the gender table is selected for translating F to W. There is a correspondence between the name of the mapping method and the name of the selected code table. Using mapGender() instructs IPF to use the gender mapping table. If you write mapXyz() IPF would try to find a table named xyz ... and so on. You may also use the map*() methods on java.lang.String objects like in the following example.

assert 'W' == 'F'.mapGender()

This is achieved via dynamic Groovy language features.

Route testing

Automated test

For automated testing we will use the following HL7 message.

msg-01.hl7
MSH|^~\&|SAP-ISH|HZL|||20040805152637||ADT^A01|123456|T|2.2|||ER
EVN|A01|20040805152637
PID|1||79471||Meier^Elfriede|Meier|19400101000000|F|||Hauptstrasse 23^^Essen^NW^11000^DE^H|||||S|||111-11-1111||||Essen
NK1|1|Meier^Elfriede|EMC|Hauptstrasse 23^^Essen^NW^11000^DE|333-4444~333-5555|
PV1|1|I|ISKA^13^4|R||||823745217||||||||N|||79237645|||||||||||||||||||||||||20040805000000

After processing we expect the following output.

msg-01.hl7.expected
MSH|^~\&|SAP-ISH|HZL|||20040805152637||ADT^A01|123456|T|2.2|||ER
EVN|A01|20040805152637
PID|1||79471||Meier^Elfriede|Meier|19400101|W|||Hauptstrasse 23^^Essen^NW^11000^DE^H|||||S|||111-11-1111||||Essen
NK1|1|Meier^Elfriede|EMC|Hauptstrasse 23^^Essen^NW^11000^DE|333-4444~333-5555|
PV1|1|I|ISKA|R||||823745217||||||||N|||79237645|||||||||||||||||||||||||20040805000000

Create two files, msg-01.hl7 and msg-01.hl7.expected, with the above content under the src/test/resources folder.

These files will be used inside our JUnit test. To implement the test open the SampleRouteTest.java file in Eclipse and replace its content with the following.

package org.openehealth.tutorial;

import static org.junit.Assert.assertEquals;

import org.apache.camel.Exchange;
import org.apache.camel.ProducerTemplate;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openehealth.ipf.modules.hl7dsl.MessageAdapters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class})
@ContextConfiguration(locations = { "/context.xml" })
public class SampleRouteTest {

    @Autowired
    private ProducerTemplate<Exchange> producerTemplate;

    @Test
    public void testRoute() throws Exception {
        Resource input = new ClassPathResource("/msg-01.hl7");
        producerTemplate.requestBody("direct:input", input.getInputStream());
        Resource result = new FileSystemResource("target/output/HZL.hl7");
        assertEquals(
                MessageAdapters.load("msg-01.hl7.expected").toString(),
                MessageAdapters.make(result.getInputStream()).toString());
    }

}

The testRoute method opens an InputStream on the input file and sends that stream in the in-message body to the direct:input endpoint. The processing result is loaded from the created HZL.hl7 file and compared with the expected processing result. For reading HL7 messages from a stream or a resource we use the MessageAdapters utility class. To execute the test right-click on SampleRouteTest.java and select Run As->JUnit Test from the context menu. The test should now successfully execute and you should also see an HZL.hl7 file in the target/output directory containing the processing result.

Just for fun, modify the validation part, so that the message does not validate against the rules. Modify the SampleRulesBuilder.groovy script and comment the EVN segment

          .forVersion('2.2')
            .message('ADT', 'A01').abstractSyntax(
                    'MSH',
                 // 'EVN',
                    'PID',
                    [  {  'NK1'  }  ],

If you run the test again, the console shows exceptions and the test fails:

...
SEVERE: Invalid message
ca.uhn.hl7v2.validation.ValidationException: The structure EVN appears in the message but not in the profile
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
...

Before continuing, undo the change in SampleRulesBuilder.groovy by removing the comment again.

Manual test

For manual testing we use the Eclipse HTTP Client to send HL7 messages to the jetty endpoint that we configured in SampleRouteBuilder.groovy. Before we can send messages over HTTP we have to start the route server. To do so, right-click on SampleServer.java and select Run As->Java Application. Now we have a server running that accepts HL7 messages on port 8080 under the tutorial context path. Then open the Eclipse HTTP client and

  • Enter http://localhost:8080/tutorial in the address field (top-left corner of the window)
  • Select POST in the HTTP method field (top-right corner of the window)
  • Copy the following HL7 message into the Body field
MSH|^~\&|SAP-ISH|PKL|||20040805152637||ADT^A01|123456|T|2.2|||ER
EVN|A01|20040805152637
PID|1||79471||Meier^Elfriede|Meier|19400101000000|F|||Hauptstrasse 23^^Essen^NW^11000^DE^H|||||S|||111-11-1111||||Essen
NK1|1|Meier^Elfriede|EMC|Hauptstrasse 23^^Essen^NW^11000^DE|333-4444~333-5555|
PV1|1|I|ISKA^13^4|R||||823745217||||||||N|||79237645|||||||||||||||||||||||||20040805000000

This is the same message we used for the JUnit test but with a different MSH[4] value (PKL instead of HZL). To submit the request press the green arrow at the top of the window. The result should look like:

Because we derive the result filename from the MSH[4] field we should now see a PKL.hl7 file in the target/output directory.

Remember to terminate the server that we started for the manual test. You can do this in Eclipse within the Console-Window by clicking the red terminate icon:

Assembly and installation

We finally want to create a distribution package from our project, install (unzip) that package somewhere and start a standalone integration server (i.e. an integration server that runs outside Eclipse). Before continuing make sure that you stopped the SampleServer that you started within Eclipse, otherwise, you won't be able to start another server. To create the package enter

mvn assembly:assembly

on the command line. The created package hl7-1.0-SNAPSHOT-bin.zip is written to the project's target folder. Copy the package to a new location and unzip it. This will create a folder named hl7-1.0-SNAPSHOT with the following content:

The lib folder contains the project jar file (hl7-1.0-SNAPSHOT.jar) as well as all required runtime dependencies. The conf folder contains a log4j.xml configuration file. Startup scripts are located directly under the root folder. The way how the project is packaged can of course be customized by changing the project's src/main/assembly/bin.xml assembly descriptor. The script startup.sh is currently empty i.e. for testing-purposes you have to run the server on Windows using startup.bat.

Start server

To start the server run startup.bat. The console output should like like

Finally, submit the HTTP request again that you've configured before with the Eclipse HTTP client. This should again create a file PKL.hl7 in the newly created target/output folder. The server can be stopped by pressing CTRL+C.

OSGi tutorial

This tutorial is organized such that we first download, install and run a sample application and then walk through its implementation details. The sample application is dealing with HL7 message processing in an OSGi environment.

Application setup

Install IPF runtime

A prerequisite for running the sample application is the installation of the IPF runtime. Refer to the IPF runtime installation and startup section for instructions how to setup the IPF runtime.

Download application bundles

An archive (tutorials-osgi.zip) containing the application bundles can be downloaded from here. These bundles are part of the IPF version 1.6 tutorials. Here's a short description.

Bundle Description
tutorials-osgi-extension-1.6.0.jar Extension bundle containing tutorial-specific DSL extensions.
tutorials-osgi-mapping-1.6.0.jar Fragment bundle containing a mapping definition for modules-hl7.
tutorials-osgi-route-file-1.6.0.jar HL7 message processing route that reads HL7 messages from a file endpoint.
tutorials-osgi-route-web-1.6.0.jar HL7 message processing route that reads HL7 messages from an HTTP endpoint.
tutorials-osgi-service-1.6.0.jar HL7 transformer that is registered in the OSGi service registry and referenced by route definitions.

The archive also contains a property file (org.openehealth.ipf.tutorials.osgi.service.cfg) for configuring the HL7 transformer service via the OSGi Configuration Admin Service.

Checkout sources (optional)

The sample application source code can be checked out from our subversion repository.

svn co --username anonymous http://gforge.openehealth.org/svn/ipf/trunk/ipf/tutorials/osgi osgi

If you want to build the bundles from the source code directly change to the created osgi folder and enter

mvn install

on the command line. For details how to setup your development environment refer to the development section of the reference manual.

Install application bundles

Extract the downloaded tutorials-osgi.zip and copy

  • all contained jar files (tutorials-osgi-*-1.6.0.jar) to the $IPF_RUNTIME_HOME/plugins/ipf directory and
  • the org.openehealth.ipf.tutorials.osgi.service.cfg file to the $IPF_RUNTIME_HOME/configuration/load directory

where $IPF_RUNTIME_HOME refers to the root directory of the installed IPF runtime. To install the application bundles you have two options. Either you add them to the $IPF_RUNTIME_HOME/configuration/main/config.ini file or you install them manually via the Equinox OSGi console. Both options are explained here although the first one is recommended because it automatically installs the application bundles after a restart of the IPF runtime.

Installation via config.ini

Either download a prepared config.ini file and overwrite the existing config.ini file from your IPF runtime installation or append the following lines to the existing config.ini (i.e. the lines following plugins/ipf/platform-camel-hl7-1.6.0.jar@start).

config.ini
...
plugins/ipf/platform-camel-hl7-1.6.0.jar@start, \
plugins/ipf/tutorials-osgi-mapping-1.6.0.jar, \
plugins/ipf/tutorials-osgi-extension-1.6.0.jar, \
plugins/ipf/tutorials-osgi-service-1.6.0.jar, \
plugins/ipf/tutorials-osgi-route-file-1.6.0.jar, \
plugins/ipf/tutorials-osgi-route-web-1.6.0.jar

Don't forget to append a comma-blank-backslash character sequence after plugins/ipf/platform-camel-hl7-1.6.0.jar@start, otherwise your additions won't be recognized. Then change to the $IPF_RUNTIME_HOME folder and enter

runtime.bat

on the command line. Type ss in the console window and you should see the IPF runtime bundles plus the sample application bundles.

Installation via console

If you prefer to install the bundles manually change to the $IPF_RUNTIME_HOME folder and enter

runtime.bat

on the comamnd line (on Windows). If you're working on Linux enter

runtime.sh

To install the bundles enter the following commands at the osgi> prompt.

install file:plugins/ipf/tutorials-osgi-mapping-1.6.0.jar
install file:plugins/ipf/tutorials-osgi-extension-1.6.0.jar
install file:plugins/ipf/tutorials-osgi-service-1.6.0.jar
install file:plugins/ipf/tutorials-osgi-route-file-1.6.0.jar
install file:plugins/ipf/tutorials-osgi-route-web-1.6.0.jar

The result of each command is the id of the newly installed bundle. Here's an example

Because tutorials-osgi-mapping is a fragment of the modules-hl7 bundle we have to refresh that bundle. In our example the bundle has the id 75. Entering

refresh 75

on the console attaches the fragment to the modules-hl7 bundle and causes the host bundle to load the mapping configuration. These refresh generates some output on the console. After typing ss you should again see the same list of installed bundles as in the previous section. Verify that the tutorials-osgi-mapping fragment (id=88) has a master with id 75.

Start application bundles

Finally we have to start the application bundles before we can use the sample application. Starting the application bundles could also be done automatically by appending a @start tag to relevant bundles in the config.ini. Here, we start the bundles step by step. The first step is to activate the DSL extensions provided by tutorials-osgi-extension (bundle id=89). The extensions are recognized by the osgi-extender-basic bundle. After typing

start 89

you should see the following output

Then we start the transformer service provided by tutorials-osgi-extension. The transformer service is a transmogrifier that is registered in the OSGi service registry and referenced by route definitions from other bundles. After starting the service with

start 90

you should see the following output

To start the message processing route that reads an HL7 message from a file endpoint start the tutorials-osgi-route-file bundle with

start 91

Here's the console output:

To start the message processing route that reads an HL7 message from an HTTP endpoint start the tutorials-osgi-route-web bundle with

start 92

Here's the console output:

Running the sample

After having started the bundles you should now see a newly created $IPF_RUNTIME/workspace/input folder for reading HL7 messages from a file. That's the input folder created by the route definition of tutorials-osgi-route-file. The route definition of tutorials-osgi-route-web creates an HTTP endpoint that accepts HL7 messages on http://localhost:8080/tutorial. For processing the HL7 message use this ADT_A01 message. The message transformation done by the sample application is as follows

  • The MSH[4] value (sending facility) is replaced by the value of the service.sending.facility property in the $RUNTIME_HOME/configuration/load/org.openehealth.ipf.tutorials.osgi.service.cfg file. Please note that changing this value at runtime doesn't automatically change the behaviour of the transformer services because Spring Dynamic Modules doesn't support updates from the Configuration Admin Service yet. To activate the new settings you have to restart the tutorial-osgi-service bundle manually (stop 90 and then start 90).
  • The gender code in PID[8] is translated to another code as given by the sample code mapping table contained in the fragment tutorial-osgi-fragment. Refer to the description of the modules-hl7 service bundle and extension bundle for details regarding custom code mappings.

Here a graphical summary of the transformation.

Read input message from file

To read the input message from the file system drop this ADT_A01 message file to the $IPF_RUNTIME/workspace/input folder. The file is read, transformed and written to the $IPF_RUNTIME/workspace/output folder. The output file (same name as the input file) should contain the proper transformation result. The input file is backed up to a $IPF_RUNTIME/workspace/input/.camel folder. Please note that when you put the original file to the input folder a second time it won't be re-read unless you touch the file i.e. change its timestamp.

Read input message via HTTP

To send the input message to the HTTP endpoint created by tutorials-osgi-route-web we use the Eclipse HTTP Client. Open the client and

To submit the request press the green arrow at the top of the window. You should now see an output.hl7 file in the $IPF_RUNTIME/workspace/output folder containing the transformation result. The HTTP client window should look like:

Flow management

Both application bundles, osgi-route-file and osgi-route-web have configured flow management for their routes (see also application walkthrough). We use the platform manager to view the flows that have been tracked during processing of the HL7 messages. Open the platform manager as described in the platform manager documentation. After having started the platform manager create a new JMX connection.

Press Finish and double-click on the connection symbol in the Connections view to open the flow management tab. Now we have to choose for which application we want to view the flows. osgi-route-file creates flows under the application name osgi-file whereas osgi-route-web creates flows under the application name osgi-web. Here, we'll look at the osgi-web flows. Enter osgi-web into the Application field and press the Apply button. Then press the Search button to search all flows for the osgi-web application. The result is a single flow because we only sent a single message through the route implemented by the osgi-tutorial-web bundle.

Since we configured flow message renderers in the route of osgi-tutorials-web we can display inbound and outbound message content in the platform manager. To display the inbound message content, right-click on the flow in the search results and select Show flow content->Inbound.

The inbound message is displayed. It is the same that we posted via HTTP.

To display the outbound message, right-click on the flow in the search results and select Show flow content->Outbound->Path 0.

The outbound message is displayed. It is the same that we see in the $IPF_RUNTIME/workspace/output folder (output.hl7). In the following figure, the transformed message fields are surrounded by a red circle.

Application walkthrough

The next figure gives an overview of the sample application's service architecture. It shows the five sample application bundles (osgi-tutorials-*) and their interaction with other bundles. Each application bundle is described in more detail in the following subsections. Before you continue make sure that you have at least a basic understanding of the IPF runtime architecture.


The tutorials-osgi-mapping bundle

This bundle is a fragment that is attached to the modules-hl7 bundle. It provides a code mapping file that is recognized the by the mapping service. For details how to contribute custom mappings to modules-hl7 refer to the description of the modules-hl7 bundle in the OSGi support section. Here's the simple mapping definition

mappings-tutorial.def
mappings = {

    gender(
        F      : 'W',
        (ELSE) : { it }
    )
         
}

It is needed to translate the gender code in our sample message. Besides the MANIFEST.MF this is the only content of the tutorials-osgi-mapping bundle. The bundle host (i.e. the bundle to which this fragment is attached) is defined in the MANIFEST.MF file.

MANIFEST.MF
...
Fragment-Host: org.openehealth.ipf.modules.modules-hl7
...

The MANIFEST.MF file has been generated using the Maven Bundle Plugin.

The tutorials-osgi-extension bundle

This bundle contributes a DSL extension that is used by osgi-tutorials-file and osgi-tutorials-web. It is the setFilename extension that these two bundles use to set the name of the output file. Here's the extension definition.

SampleExtension.groovy
import org.apache.camel.model.ProcessorType

class SampleExtension {

     static extensions = {
         
         ProcessorType.metaClass.setFilename = {String filename ->
             delegate.setHeader('org.apache.camel.file.name') {filename}
         }
         
     }
    
}

It hides the details of Camel-specific header names from the application developer. Usage of this extension is shown in the route definitions of the tutorials-osgi-route-file and the tutorials-osgi-route-web bundles. In order to be recognized by an IPF extender bundle this extension class must be added to the (IPF-specific) Extension-Classes manifest header.

MANIFEST.MF
...
Extension-Classes: org.openehealth.ipf.tutorials.osgi.extension.SampleExtension
...

The MANIFEST.MF file has been generated using the Maven Bundle Plugin.

The tutorials-osgi-service bundle

This bundle implements the HL7 transformation logic and provides it as a service in the OSGi service registry. This service is then referenced by osgi-tutorials-route-file and osgi-tutorials-route-web. Here's the service implementation.

AdmissionTransmogrifier.groovy
import org.openehealth.ipf.commons.core.modules.api.Transmogrifier

class AdmissionTransmogrifier implements Transmogrifier {

    String sendingFacility = 'UNK'
     
    Object zap(Object msg, Object[] params) {
        msg.MSH[4] = sendingFacility
        msg.PID[8] = msg.PID[8].mapGender()
        msg
    }
    
    ...
    
}

It is a transmogrifier that uses the HAPI DSL and HAPI extensions for transforming the HL7 message. It defines a sendingFacility property for setting the MSH[4] field of the input message. The value of the sendingFacility property is set via the Configuration Admin Service (see later). The gender code in PID[8] (F in our example) is translated according to the gender mapping table that is contributed by the tutorials-osgi-mapping bundle. With the mapGender() method we can do the code mapping using this table. For details how code mapping works refer to the HL7 processing tutorial and the description of the modules-hl7 bundle in the OSGi support section.

The service is configured with two Spring application context files

  • META-INF/spring/service-context.xml (OSGi-independent)
  • META-INF/spring/service-osgi-context.xml (OSGi-dependent)

The files are recognized and activated by the Spring Dynamic Modules extender when the hosting bundle is started. The service-context.xml application context defines the admissionTransmogrifier bean.

service-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

    ...

    <bean id="admissionTransmogrifier" 
        class="org.openehealth.ipf.tutorials.osgi.service.impl.AdmissionTransmogrifier">
        <property name="sendingFacility" value= "${service.sending.facility}" />
    </bean>
 
    ...

</beans>

The sendingFacility property value is a placeholder that is substituted by a value that is retrieved from the Configuration Admin Service. The Configuration Admin Service stores the service.sending.facility property under the org.openehealth.ipf.tutorials.osgi.service persistent id (see below). The service bundle retrieves the property value via a Spring-DM-specific property placeholder configurer. This placeholder configurer interacts with the Comfiguration Admin Service and is defined as osgix:property-placeholder in the service-osgi-context.xml application context.

service-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:osgi="http://www.springframework.org/schema/osgi"
    xmlns:osgix="http://www.springframework.org/schema/osgi-compendium"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd
http://www.springframework.org/schema/osgi-compendium
http://www.springframework.org/schema/osgi-compendium/spring-osgi-compendium.xsd">

    <!--
        Obtain config from configuration admin service.
     -->
    <osgix:property-placeholder 
        persistent-id="org.openehealth.ipf.tutorials.osgi.service">
    </osgix:property-placeholder>

    ...

</beans>

Who actually stores the service.sending.facility property in the Configuration Admin? We use the fileinstall bundle from the Apache Felix project to do that. The fileinstall bundle reads properties from a file and derives the persistent id from the filename. The properties contained in that file are then stored under the derived persistent id at the Configuration Admin. In our example, the properties file is located at $RUNTIME_HOME/configuration/load/ and the filename is org.openehealth.ipf.tutorials.osgi.service.cfg. The derived persistent id is org.openehealth.ipf.tutorials.osgi.service. Here's the content of the properties file.

org.openehealth.ipf.tutorials.osgi.service.cfg
service.sending.facility=ABC

The value of the service.sending.facility property is ABC. In the transformed messages you see exactly that value in the MSH[4] field. Finally, to register the admissionTransmogrifer service at the OSGi service registry we define an osgi:service in the application context and reference the admissionTransmogrifier bean with the ref attribute.

service-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:osgi="http://www.springframework.org/schema/osgi"
    xmlns:osgix="http://www.springframework.org/schema/osgi-compendium"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd
http://www.springframework.org/schema/osgi-compendium
http://www.springframework.org/schema/osgi-compendium/spring-osgi-compendium.xsd">

    ...

    <osgi:service ref="admissionTransmogrifier" 
        interface="org.openehealth.ipf.commons.core.modules.api.Transmogrifier">
    </osgi:service>

</beans>

The tutorials-osgi-route-web bundle

This bundle implements an IPF route that is shown in the next figure. This route is able to process HL7 admission event messages.

In this route the example message is

  • received via an HTTP endpoint
  • validated using IPF's HL7 validation DSL
  • transformed using IPF's HAPI DSL and HAPI extensions
  • queued for delivery to its destination
  • written to a destination file (output.hl7)

Although it doesn't make much sense to put a JMS queue in front of a file endpoint, we did it to demonstrate usage of JMS endpoints in OSGi environments. Here's the code of this bundle's route builder.

SampleRouteBuilder.groovy
import org.apache.camel.spring.SpringRouteBuilder

public class SampleRouteBuilder extends SpringRouteBuilder {

     void configure() {

         from('jetty:http://0.0.0.0:8080/tutorial')   // recieve message via HTTP
             .initFlow(this.class.package.name)       // start flow management
                 .application('osgi-web')             // ... for this app name
                 .renderer('initRenderer')            // render inbound message
             .unmarshal().ghl7()                      // create message object that supports the HAPI DSL
             .validate().ghl7()                       // validate the HL7 message
             .transmogrify('admissionTransmogrifier') // transform message using admissionTransmogrifier from OSGi service registry
             .dedupeFlow()                            // install flow duplicate filter to avoid duplicates when re-playing messages
             .marshal().ghl7()                        // create external message format from internal message object
             .inOnly()                                // change exchange pattern to in-only
             .to('jms:queue:delivery-web')            // write message to queue
             
         from('jms:queue:delivery-web')               // consume message from queue
             .setFilename('output.hl7')               // set filename using DSL extension provided by osgi-tutorials-extension bundle
             .to('file:workspace/output?append=false&autoCreate=false') // write file to destination folder
             .ackFlow().renderer('ackRenderer')       // confirm sucessful delivery of message and render outbound message

     }
    
}

The admissionTransmogrifier used by this route is obtained from the OSGi service registry. To create a local service proxy for that service we create a service reference using Spring Dynamic Modules.

route-web-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:osgi="http://www.springframework.org/schema/osgi"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    <osgi:reference id="admissionTransmogrifier" bean-name="admissionTransmogrifier" timeout="10000" 
        interface="org.openehealth.ipf.commons.core.modules.api.Transmogrifier">
    </osgi:reference>

    ...

</beans>

Since there could be potentially many services implementing Transmogrifier in the service registry we also constrain this reference to the admissionTransmogrifier bean name (it was registered by tutorials-osgi-service under this name). The id attribute gives the local proxy the bean name admissionTransmogrifier to which the route refers.

To make flow management and flow message rendering work we define the following beans in the application context.

route-web-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:osgi="http://www.springframework.org/schema/osgi"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    ...

    <osgi:reference id="flowManager" timeout="10000" 
        interface="org.openehealth.ipf.commons.flow.FlowManager">
    </osgi:reference>

    <bean class="org.openehealth.ipf.platform.camel.flow.osgi.OsgiReplayStrategyRegistry" />

    ...

</beans>

This references the flowManager service in the OSGi service registry and creates a local interface for registering flow replay strategies at the OSGi service registry. For details refer to the flow management section of the OSGi support section. For the jms component we also create a local service reference. This component was added to the service registry by the IPF runtime (i.e. the osgi-config-jms bundle).

route-web-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:osgi="http://www.springframework.org/schema/osgi"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    ...

    <osgi:reference id="jms" bean-name="jms" timeout="10000" 
        interface="org.apache.camel.Component">
    </osgi:reference>

    ...

</beans>

The route builder, the flow message renderers and the Camel context are defined locally in META-INF/spring/route-web-context.xml.

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

    <camel:camelContext id="camelContext" />

    <bean id="routeBuilder" 
        class="org.openehealth.ipf.tutorials.osgi.route.web.SampleRouteBuilder">
    </bean>

    <bean id="initRenderer"
        class="org.openehealth.ipf.tutorials.osgi.route.web.render.InitRenderer">
    </bean>

    <bean id="ackRenderer"
        class="org.openehealth.ipf.tutorials.osgi.route.web.render.AckRenderer">
    </bean>

</beans>

The InitRenderer creates a String representation from a ByteArrayInputStream that contains the inbound message. This string representation is then stored by the flow manager. The input stream is created by the jetty component. After reading from the stream it is reset in order to allow other processors to read from it again.

InitRenderer.groovy
import org.openehealth.ipf.platform.camel.flow.PlatformMessage
import org.openehealth.ipf.platform.camel.flow.PlatformMessageRenderer

class InitRenderer implements PlatformMessageRenderer {

     @Override
     String render(PlatformMessage message) {
         try {
             return message.exchange.in.getBody(String.class)
         } finally {
             // reset ByteArrayInputStream
             message.exchange.in.body.reset()
         }
         
     }
    
}

The AckRenderer renders the outbound message. For proper display it replaces \r characters by \n.

InitRenderer.groovy
import org.openehealth.ipf.platform.camel.flow.PlatformMessage
import org.openehealth.ipf.platform.camel.flow.PlatformMessageRenderer

class AckRenderer implements PlatformMessageRenderer {

     @Override
     String render(PlatformMessage message) {
         message.exchange.in.getBody(String.class).replaceAll('\r', '\n')
     }
    
}

The tutorials-osgi-route-file bundle

This bundle is almost identical to tutorials-osgi-route-web except that

  • the inbound message is read from the $IPF_RUNTIME/workspace/input folder
  • the name of the application where the flow manager stores flows is osgi-file
  • the name of the created file in the output folder is derived from the input file name
  • the flow management interceptors aren't configured with flow message renderers
  • the queue name created by the central message broker is delivery-file

Here's the graphical representation and the code of the route implemented by this bundle.

SampleRouteBuilder.groovy
import org.apache.camel.spring.SpringRouteBuilder

public class SampleRouteBuilder extends SpringRouteBuilder {

     void configure() {

         from('file:workspace/input?lock=false')
             .initFlow(this.class.package.name).application('osgi-file')
             .unmarshal().ghl7()
             .validate().ghl7()
             .transmogrify('admissionTransmogrifier')
             .dedupeFlow()
             .marshal().ghl7()
             .to('jms:queue:delivery-file')
             
         from('jms:queue:delivery-file')
             .to('file:workspace/output?append=false&autoCreate=false')
             .ackFlow()

     }
    
}

Tutorial for routing to a webservice via HTTP

Prerequisites

This tutorial shows how to use the Large Binary Support (LBS) of the IPF to create a router for a webservice that exposes an HTTP-based protocol. It is targeted at developers that are familiar with the IPF, Camel and Groovy. A basic understanding of webservices based on a wsdl is also useful, although not essential.

The webservice is a simple image repository that allows uploading and downloading of images. Using the IPF we will expose this webservice via typical HTTP POST and GET requests.

The steps of the tutorial:

Source code

The source code for this tutorial can be downloaded from here. Unpack the zip and import the contained Eclipse project into your workspace. After importing it might be necessary to clean the project if errors are reported.

Create a basic project using the IPF and LBS

We use an IPF archetype to create the Maven project. Find a suitable location on your disk and create the project (make sure that the command is on a single line):

mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate 
-DarchetypeGroupId=org.openehealth.ipf.archetypes 
-DarchetypeArtifactId=ipf-archetype-basic 
-DarchetypeVersion=1.7.0
-DgroupId=org.openehealth.tutorial 
-DartifactId=router 
-Dversion=1.0-SNAPSHOT 
-DinteractiveMode=false

Note: Depending on the version of the IPF you are using, you have to change the archetypeVersion setting.

Within an Eclipse workspace you can now import the project using File/Import/General/Existing Projects into Workspace. Select the router directory and click Finish. You should see the router project in the workspace.

The created project already contains a useful skeleton, but it must be configured to use the LBS. Open the pom.xml and add the following dependencies to the <project><dependencies> section:

...
        <dependency>
            <groupId>org.openehealth.ipf.platform-camel</groupId>
            <artifactId>platform-camel-lbs-cxf</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.openehealth.ipf.platform-camel</groupId>
            <artifactId>platform-camel-lbs-http</artifactId>
            <version>1.7.0</version>
        </dependency>

Save the file and the Maven Eclipse plugin updates the dependencies to include the LBS jars.

The archetype project contains a sample configuration that is based on the core IPF. We want to change this configuration to use the extensions of the LBS, allowing us to use the store and fetch processors. Open the file context.xml in src/main/resources. This is the configuration file of the Spring application context. It contains the extensions available for route definitions. The sample configuration includes two extensions: coreModelExtension for the core IPF features and sampleModelExtension for any custom extensions of our own project. We need to add the extension of the LBS by adding another bean:

context.xml
...
    <!-- Allows usage of store() and fetch() within routes -->
    <bean id="lbsModelExtension" 
        class="org.openehealth.ipf.platform.camel.lbs.core.extend.LbsModelExtension">
    </bean>

All extension beans must be registered with a ModelExtender, in this case the routeModelExtender. Add the new bean to the list of extensions. The result should look like this:

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

Finally we add a bean for a LargeBinaryStore. This is the store that contains images while they are being processed by the router. We also add the ResourceFactory that is used by the routes to create resources:

context.xml
...
    <!-- Stores the binaries while processing the routes -->
    <bean id="largeBinaryStore" class="org.openehealth.ipf.commons.lbs.store.DiskStore">
        <constructor-arg value="target/tempstore"/>
    </bean>

    <!-- Creates data sources used as resources in Camel messages -->
    <bean id="resourceFactory" class="org.openehealth.ipf.commons.lbs.resource.ResourceFactory">
        <constructor-arg ref="largeBinaryStore" />
        <constructor-arg value="unnamed" />
    </bean>

The project is now configured to use the LBS.

Create the webservice

We are using a wsdl-first approach for the webservice. The service is quite simple. It should allow uploading and downloading of images via two methods. In the wsdl we define a service with these two methods and a type for the image. The upload method returns a handle for an image. We can use this handle to download the image again later. The handle is simply a String.

Create the following wsdl-file in the directory src/main/resources/wsdl and name it imagebin.wsdl:

/router/src/main/resources/wsdl/imagebin.wsdl
<wsdl:definitions name="ImageBin"
	targetNamespace="http://tutorial.openehealth.org/imagebin/"
	xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
	xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:tns="http://tutorial.openehealth.org/imagebin/"
	xmlns:types="http://tutorial.openehealth.org/imagebin/types/"
	xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<wsdl:types>
		<schema targetNamespace="http://tutorial.openehealth.org/imagebin/types/"
			xmlns="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://tutorial.openehealth.org/imagebin/types/"
			elementFormDefault="qualified">

			<element name="upload">
				<complexType>
					<sequence>
						<element name="imageData" type="base64Binary"
							xmime:expectedContentTypes="application/octet-stream" />
					</sequence>
				</complexType>
			</element>

			<element name="uploadResponse">
				<complexType>
					<sequence>
						<element name="handle" type="string" />
					</sequence>
				</complexType>
			</element>

			<element name="download">
				<complexType>
					<sequence>
						<element name="handle" type="string" />
					</sequence>
				</complexType>
			</element>

			<element name="downloadResponse">
				<complexType>
					<sequence>
						<element name="imageData" type="base64Binary"
							xmime:expectedContentTypes="application/octet-stream" />
					</sequence>
				</complexType>
			</element>

		</schema>
	</wsdl:types>

	<wsdl:message name="uploadRequest">
		<wsdl:part element="types:upload" name="in" />
	</wsdl:message>

	<wsdl:message name="uploadResponse">
		<wsdl:part element="types:uploadResponse" name="out" />
	</wsdl:message>

	<wsdl:message name="downloadRequest">
		<wsdl:part element="types:download" name="in" />
	</wsdl:message>

	<wsdl:message name="downloadResponse">
		<wsdl:part element="types:downloadResponse" name="out" />
	</wsdl:message>

	<wsdl:portType name="ImageBin">
		<wsdl:operation name="upload">
			<wsdl:input message="tns:uploadRequest" name="uploadRequest" />
			<wsdl:output message="tns:uploadResponse" name="uploadResponse" />
		</wsdl:operation>

		<wsdl:operation name="download">
			<wsdl:input message="tns:downloadRequest" name="downloadRequest" />
			<wsdl:output message="tns:downloadResponse" name="downloadResponse" />
		</wsdl:operation>
	</wsdl:portType>

	<wsdl:binding name="ImageBin_SOAPBinding" type="tns:ImageBin">
		<soap:binding style="document"
			transport="http://schemas.xmlsoap.org/soap/http" />

		<wsdl:operation name="upload">
			<soap:operation soapAction="" style="document" />

			<wsdl:input name="uploadRequest">
				<soap:body use="literal" />
			</wsdl:input>

			<wsdl:output name="uploadResponse">
				<soap:body use="literal" />
			</wsdl:output>
		</wsdl:operation>

        <wsdl:operation name="download">
            <soap:operation soapAction="" style="document" />

            <wsdl:input name="downloadRequest">
                <soap:body use="literal" />
            </wsdl:input>

            <wsdl:output name="downloadResponse">
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
	</wsdl:binding>

	<wsdl:service name="ImageBinService">
		<wsdl:port binding="tns:ImageBin_SOAPBinding" name="ImageBin">
			<soap:address location="http://localhost:9000/ImageBin/ImageBinPort" />
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>

Now we add a build step to the pom.xml to generate the service-related classes from the wsdl file using wsdl2java. Open the pom.xml and add the build plugin to the <project><build><plugins> section:

pom.xml
...
            <plugin>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-codegen-plugin</artifactId>
                <version>2.1.3</version>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>generate-sources</phase>
                        <configuration>
                            <sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
                            <wsdlOptions>
                                <wsdlOption>
                                    <wsdl>${basedir}/src/main/resources/wsdl/imagebin.wsdl</wsdl>
                                </wsdlOption>
                            </wsdlOptions>
                        </configuration>
                        <goals>
                            <goal>wsdl2java</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

To generate the java classes for the wsdl we must run Maven on the command line. In the project root directory run:

mvn install

The generated sources are now inside target/generated/src/main/java. This directory has to be added to the source directories in Eclipse. Switch back to Eclipse and refresh the project tree. Right click on the project and choose Properties, then Java Build Path and select the Source tab. Press the button Add Folder and select target/generated/src/main/java in the folder selection. Exits the dialogs with OK. Now you should have a new source folder that contains the stubs for our ImageBin webservice.

Create a new class via File/New/Class. Put it in the package org.openehealth.tutorial.imagebin and call it ImageBinImpl. Also choose the interface org.openehealth.tutorial.imagebin.ImageBin and press Finish.

In our new class we add a LargeBinaryStore from the IPF to store the uploaded images. Adding an image to the store is a simple matter of handing the store the input stream from the upload parameter. Downloading is not much harder. We need to create a DataSource that gets its input stream from the store.

The resulting implementation also configures the class to be used as a webservice using the @WebService annotation:

/router/src/main/java/org/openehealth/tutorial/imagebin/ImageBinImpl.java
package org.openehealth.tutorial.imagebin;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;

import javax.activation.DataHandler;
import javax.activation.DataSource;

import org.openehealth.ipf.commons.lbs.store.DiskStore;
import org.openehealth.ipf.commons.lbs.store.LargeBinaryStore;
import org.openehealth.tutorial.imagebin.ImageBin;

import javax.jws.WebService;

@WebService(portName = "ImageBin", serviceName = "ImageBinService", 
        targetNamespace = "http://tutorial.openehealth.org/imagebin/", 
        endpointInterface = "org.openehealth.tutorial.imagebin.ImageBin",
        wsdlLocation = "wsdl/imagebin.wsdl")
public class ImageBinImpl implements ImageBin {
	// This is the store where we save our uploaded images
	private final LargeBinaryStore store;

	// Create a store located at a specific path on disk
	public ImageBinImpl(String storeLocation) {
		store = new DiskStore(storeLocation);
	}

	public DataHandler download(final String handle) {
		// Create a data handler and source that retrieve the input stream from the store
		return new DataHandler(new DataSource() {
			public String getContentType() {
				return "application/octet-stream";
			}

			public InputStream getInputStream() throws IOException {
				return store.getInputStream(URI.create(handle));
			}

			public String getName() {
				return "image";
			}

			public OutputStream getOutputStream() throws IOException {
				throw new UnsupportedOperationException();
			}			
		});
	}

	public String upload(DataHandler imageData) {
		// Use the input stream in the handler to add it to the store
		try {
			InputStream inputStream = imageData.getInputStream();
			URI resourceUri = store.add(inputStream);
			inputStream.close();
			return resourceUri.toString();
		}
		catch (IOException e) {
			// Not properly handled, but ok for now
			e.printStackTrace();
		}
		return "";
	}
}

Please note: We do not properly handle exceptions in this code to keep it as small as possible.

We also add a class to start and stop the CXF service. Add a new class to the main java sources called ImageBinServer in the org.openehealth.tutorial.imagebin package:

/router/src/main/java/org/openehealth/tutorial/imagebin/ImageBinServer.java
package org.openehealth.tutorial.imagebin;

import java.io.File;
import javax.xml.ws.Endpoint;
import javax.xml.ws.soap.SOAPBinding;

public class ImageBinServer {
	private Endpoint imageBinEndpoint;

	public void start() {
		File directory = new File("target/store");
		directory.mkdir();
		
		System.out.println("Starting ImageBin Server");
		
		// Publish the service
		Object imageBin = new ImageBinImpl(directory.getAbsolutePath());
		String address = "http://localhost:9000/ImageBin/ImageBinPort";
		imageBinEndpoint = Endpoint.publish(address, imageBin);
		
		// Enable MTOM attachments
		SOAPBinding binding = (SOAPBinding) imageBinEndpoint.getBinding();
		binding.setMTOMEnabled(true);
		
		System.out.println("ImageBin ready...");
	}
	
	public void stop() { 		
		System.out.println("ImageBin exiting");        
		imageBinEndpoint.stop();
	}
}

This should be tested by a simple test case that calls upload and download on the running webservice. Create a new class in the test directory src/test/java. Use the package org.openehealth.tutorial.imagebin and the name ImageBinServerTest. The test case could be something like this (we are using helper classes from the org.apache.commons.io package):

/router/src/test/java/org/openehealth/tutorial/imagebin/ImageBinServerTest.java
package org.openehealth.tutorial.imagebin;

import static org.junit.Assert.*;

import java.io.File;
import java.io.InputStream;
import javax.activation.DataHandler;
import javax.mail.util.ByteArrayDataSource;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openehealth.tutorial.imagebin.ImageBin;
import org.openehealth.tutorial.imagebin.ImageBinServer;
import org.openehealth.tutorial.imagebin.ImageBinService;

public class ImageBinServerTest {
	private ImageBinServer imageBinServer;

	@Before
	public void setUp() throws Exception {
		// Make sure a previously created store is removed
		File storeLocation = new File("target/store");
		FileUtils.deleteDirectory(storeLocation);
		
		// Start the CXF webservice
		imageBinServer = new ImageBinServer();
		imageBinServer.start();
	}
	
	@After
	public void tearDown() throws Exception {
		imageBinServer.stop();
	}
	
	@Test
	public void testUpAndDownload() throws Exception {
		// Create a client interface to the CXF webservice
		ImageBinService service = new ImageBinService();		
		ImageBin imageBin = service.getImageBin();

		// Image data doesn't need to be a real image
		byte[] imageData = "TestImage".getBytes();

		// Call the service to upload the image
		ByteArrayDataSource dataSource = new ByteArrayDataSource(imageData, "application/octet-stream");
		DataHandler myImage = new DataHandler(dataSource);
		String handle = imageBin.upload(myImage);
		
		// Download the image again
		DataHandler downloadedImage = imageBin.download(handle);
		
		// And check if we received the image data
		InputStream inputStream = downloadedImage.getInputStream();
		assertTrue("Image data is not equal", 
				IOUtils.contentEquals(myImage.getInputStream(), inputStream));
		inputStream.close();
	}
}

Running the test should work and leave a file in the router/target/store directory. You can open the file and see the uploaded content (TestImage).

Add the routing

We now want to expose the webservice via an HTTP endpoint. To do so we need to create a route via Camel. The route connects the HTTP endpoint with the CXF endpoint of our webservice.

The first thing to do is to add a CXF endpoint. Open context.xml and add a bean for the endpoint, mapping it to the service.

context.xml
...
    <cxf:cxfEndpoint id="imageBinServer"
        serviceClass="org.openehealth.tutorial.imagebin.ImageBin"
        address="http://localhost:9000/ImageBin/ImageBinPort"
        endpointName="s:ImageBin" serviceName="s:ImageBinService" wsdlURL="wsdl/imagebin.wsdl"
        xmlns:s="http://tutorial.openehealth.org/imagebin/">
        <cxf:properties>
            <entry key="mtom-enabled" value="true" />
        </cxf:properties>
    </cxf:cxfEndpoint>

This requires that we import a few CXF-related Camel resources and define the cxf namespace. Make sure that the beans tag of the context.xml file looks like the following code:

context.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xmlns:camel="http://activemq.apache.org/camel/schema/spring"
       xmlns:cxf="http://activemq.apache.org/camel/schema/cxfEndpoint"             
       xmlns:util="http://www.springframework.org/schema/util"
       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://www.springframework.org/schema/util                                         
http://www.springframework.org/schema/util/spring-util-2.5.xsd                     
http://activemq.apache.org/camel/schema/cxfEndpoint                                
http://activemq.apache.org/camel/schema/cxfEndpoint/camel-cxf.xsd                  
http://activemq.apache.org/camel/schema/spring 
http://activemq.apache.org/camel/schema/spring/camel-spring.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-extension-http-jetty.xml" />  
...

Also add a list of resource handlers as a bean to context.xml that we use in the route for handling HTTP and CXF resources:

context.xml
...
    <!-- This bean is a list of resource handlers. Add all handlers used within the routes to this list -->
    <util:list id="resourceHandlers">
        <bean class="org.openehealth.ipf.platform.camel.lbs.cxf.process.CxfPojoResourceHandler">
            <constructor-arg ref="resourceFactory" />
        </bean>
        <bean class="org.openehealth.ipf.platform.camel.lbs.http.process.HttpResourceHandler">
            <constructor-arg ref="resourceFactory" />
        </bean>
    </util:list>

This completes the configuration and we can start to implement the route.

The router project already has a sample route written in Groovy. You can find it in src/main/groovy/org/openehealth/tutorial/SampleRouteBuilder.groovy. Open the file and remove the sample routes in the configure method. Now add a new route that accepts messages from a jetty endpoint and routes them to a CXF endpoint using the request methods of the HTTP requests:

/router/src/main/groovy/org/openehealth/tutorial/SampleRouteBuilder.groovy
package org.openehealth.tutorial

import org.apache.camel.spring.SpringRouteBuilder
import org.apache.camel.Exchange
import org.apache.cxf.message.MessageContentsList
import javax.activation.DataHandler
import javax.activation.DataSource
import org.apache.camel.component.cxf.CxfConstants
import org.openehealth.ipf.platform.camel.lbs.http.process.ResourceList


public class SampleRouteBuilder extends SpringRouteBuilder {
    
    void configure() {
        // When the jetty endpoint receives a message the route checks the header 
        // for the request method.
        // The request method in the header is used to find out if we have a POST 
        // or GET request.
        // Depending on the request, we route the message to a "direct" endpoint.
        from('jetty:http://localhost:8080/imagebin')
                .disableStreamCaching()              // tell Camel to not read the stream in memory
                .choice()   
                .when(header('http.requestMethod').isEqualTo('POST')).to('direct:upload')
                .when(header('http.requestMethod').isEqualTo('GET')).to('direct:download')
                .otherwise().end()
        
        // Deal with uploads
        from('direct:upload')
                .disableStreamCaching()              // tell Camel to not read the stream in memory
                .store().with('resourceHandlers')    // ensure we can upload large files
                .process { Exchange exchange ->
                    // Transform the message into the CXF format
                    def params = new MessageContentsList()                    
                    def resourceList = exchange.in.getBody(ResourceList.class) 
                    params[0] = new DataHandler(resourceList.get(0))
                    exchange.in.setHeader(CxfConstants.OPERATION_NAME, "upload")
                    exchange.in.body = params
                }
                .to('cxf:bean:imageBinServer')       // webservice.upload() call
                .process { Exchange exchange ->
                    // Transform the message back to HTTP
                    def params = exchange.in.getBody(MessageContentsList.class)
                    exchange.in.body = params[0]
                }
        
        // Deal with downloads
        from('direct:download')
                .disableStreamCaching()              // tell Camel to not read the stream in memory
                .process { Exchange exchange ->
                    // Transform the message into the CXF format
                    def params = new MessageContentsList()
                    params[0] = exchange.in.getHeader("handle")
                    exchange.in.setHeader(CxfConstants.OPERATION_NAME, "download")
                    exchange.in.body = params
                }
                .to('cxf:bean:imageBinServer')       // webservice.download() call
                .store().with('resourceHandlers')    // ensure we can download large files
                .process { Exchange exchange ->
                    // Transform the message back to HTTP
                    def params = exchange.in.getBody(MessageContentsList.class)
                    def resourceList = new ResourceList()
                    resourceList.add(params[0].dataSource)
                    exchange.in.body = resourceList
                }
    }    
}

Most of this route is handling the transformation between HTTP and CXF requests. Note how the store processor is used to enable support for large binaries without keeping the complete image in memory.

In the upload part of the route we store the image in the temporary route store as soon as we identfied the HTTP POST message. In the download part we do the same after we called the webservice.

The two calls to the store processor use the resource handlers that we configured in context.xml.

Stream Caching

Beginning with Camel 1.6 stream caching is enabled by default. A cached stream is kept in memory to allow efficient re-readability of the stream. To prevent the LBS from using an already in-memory stream it is necessary to disable stream caching. This is done by calling disableStreamCaching() on the routes (as shown in the above example).

Of course, we should add a test for the route. Open SampleRouteTest.java in src/test/java/org/openehealth/tutorial. Remove the methods in the existing class. They test the sample route that we removed earlier.

We use the Apache HttpClient to send a real test message to the route:

/router/src/test/java/org/openehealth/tutorial/SampleRouteTest.java
package org.openehealth.tutorial;

import static org.junit.Assert.*;

import java.io.File;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openehealth.tutorial.imagebin.ImageBinServer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class })
@ContextConfiguration(locations = { "/context.xml" })
public class SampleRouteTest {
	private ImageBinServer server;
	private HttpClient client;

	// Setup that is run before the application context is loaded
	@BeforeClass
	public static void setUpBeforeClass() throws Exception {
		// There are two stores used in this test. The one from the webservice
		// that contains the image repository and the the one from the route
		// that store the image while they are uploaded  
		
		// Make sure any previously created stores are removed
		File storeLocation = new File("target/store");
		FileUtils.deleteDirectory(storeLocation);

		File routeStoreLocation = new File("target/tempstore");
		FileUtils.deleteDirectory(routeStoreLocation);
		
	}

	@Before
	public void setUp() throws Exception {
		// Start the CXF webservice
		server = new ImageBinServer();
		server.start();

		// Create the HTTP client
		client = new HttpClient();
	}
	
	@After
	public void tearDown() {
		server.stop();
	}

	@Test
	public void testUploadAndDownload() throws Exception {
		// Create a post request containing a "fake" image
		PostMethod post = new PostMethod("http://localhost:8080/imagebin");
		StringRequestEntity requestEntity = 
			new StringRequestEntity("TestImage", "application/octet-stream", null);		
		post.setRequestEntity(requestEntity);
		
		// Call the HTTP endpoint and trigger the upload part of the route
		client.executeMethod(post);
		String handle = post.getResponseBodyAsString();
		post.releaseConnection();

		// Call the HTTP endpoint and trigger the download part of the route
		GetMethod get = new GetMethod("http://localhost:8080/imagebin");
		get.setQueryString("handle=" + handle);
		client.executeMethod(get);		
		String imageAsString = get.getResponseBodyAsString();
		get.releaseConnection();

		// Check the download result
		assertEquals("TestImage", imageAsString);
	}
}

Now run the test. This shows the route in action with a started webservice. As a result you should again find the uploaded file in the target/store directory.

Reference application

Preliminary content

This section is work in progress.

The reference application is an IPF application that demonstrates how to use a broad range of IPF and Camel features. It is an example from the order processing domain. It consumes (over-simplified) order requests over HTTP and transforms these request messages into a different format depending on the order category. The transformation results are written to the filesystem. The reference application can be found under tutorials/ref in the IPF source tree. The Eclipse project name is org.openehealth.ipf.tutorials.ref. For instructions how to import IPF sources into Eclipse refer to the Development section of the reference manual. To start a single instance of the reference application you need to start a Derby database server and an IPF instance. To start the database server select Run->Run Configurations... from the Eclipse menu, choose Java Application->Derby - Start in the dialog box and press the Run button. You should see the following output on the console (German language settings).

Apache Derby Network Server 10.4.1.3 - (648739) wurde gestartet und ist seit 2008-12-04 08:01:01.541 GMT bereit, Verbindungen am Port 1527 zu akzeptieren. 

To start the IPF instance select Run->Run Configurations... from the Eclipse menu, choose Java Application->TutorialServer1 in the dialog box and press the Run button. You should see the following output on the console (warnings omitted).

INFO - using stored sequence default
INFO - Using Persistence Adapter: AMQPersistenceAdapter(data\activemq1)
INFO - ActiveMQ null JMS Message Broker (broker1) is starting
INFO - For help or more information please see: http://activemq.apache.org/
INFO - AMQStore starting using directory: data\activemq1
INFO - JMX consoles can connect to service:jmx:rmi:///jndi/rmi://localhost:1801/jmxrmi
INFO - Kaha Store using data directory data\activemq1\kr-store\state
INFO - Active data files: []
WARN - The ReferenceStore is not valid - recovering ...
INFO - Kaha Store successfully deleted data directory data\activemq1\kr-store\data
INFO - Journal Recovery Started from: DataManager:(data-)
INFO - Kaha Store using data directory data\activemq1\kr-store\data
INFO - Recovered 28 operations from redo log in 0.328 seconds.
INFO - Finished recovering the ReferenceStore
INFO - ActiveMQ JMS Message Broker (broker1, ID:lap-xp-0818-3028-1228379536237-0:0) started
...
INFO - Registered replay strategy with identifier http
INFO - Registered replay strategy with identifier file
INFO - Registered replay strategy with identifier direct
2008-12-04 09:32:20.751::INFO:  jetty-6.1.11
2008-12-04 09:32:20.876::INFO:  Started SelectChannelConnector@0.0.0.0:8081
INFO - Connector vm://broker1 Started

Now the reference application is ready to accept order request messages via http://localhost:8081/tutorial. Here are two sample order request messages and their transformation results.

Request message Transformed message
<order xmlns="http://www.openehealth.org/tutorial">
    <customer>123</customer>
    <category>animals</category>
    <item>ozelot</item>
    <count>3</count>
</order>
Order
-----
Customer: 123
Item:     ozelot
Count:    3
<order xmlns="http://www.openehealth.org/tutorial">
    <customer>123</customer>
    <category>books</category>
    <item>eating ozelots</item>
    <count>3</count>
</order>
<?xml version="1.0" encoding="UTF-8"?>
<ns0:order xmlns:ns0="http://www.openehealth.org/tutorial" category="books">
    <ns1:customer xmlns:ns1="http://www.openehealth.org/tutorial">123</ns1:customer>
    
    <ns2:item xmlns:ns2="http://www.openehealth.org/tutorial">eating ozelots</ns2:item>
    <ns3:count xmlns:ns3="http://www.openehealth.org/tutorial">3</ns3:count>
</ns0:order>

The transformed messages are written to the order/out folder if processing was successful otherwise to the order/error-app or order/error-sys folder. For sending these messages you can use the Eclipse HTTP client. Here's a screenshot made after sending an order request.

The reference application also implements the flow manager which can be accessed via the Eclipse flow management client or a generic JMX client. To connect to the flow management service with the flow management client:

The flow management window opens. To get a list of all tracked message flows so far press the Search button. Depending on the number of order requests you've sent you'll see more or less flows in the result list of the Search view. For more information on how to use the flow management client refer to the platform manager section of the reference manual.

XDS demo repository

Preliminary content

This section is work in progress.

This tutorial is a guide to the XDS demo repository, a simplified implementation of an XDS registry and repository to store documents, folders, submission sets and associations. The demo is useful for everyone who wants to use the IPF XDS components. It shows how to:

  • Use the XDS.b components to offer a registry and a repository service
  • Transform and process registration, retrieval and query requests
  • Configure and use ATNA logging
  • Enable secure transport using HTTPS

The demo repository is non-persistent. A restart will therefore always start out with a blank respository/registry.

Overview

The XDS demo repository is implemented in Groovy. Most of the code deals with the query functionality. This is of course used for the ITI-18 transaction (registry stored query), but is also used for the other transactions to perform checks of the input data. Within the project you can find the following source files, tests and configuration files.

Source files:

DataStore The actual storage of documents, document entries, folders, submission sets and associations. The store is non-persistent at the moment to keep things simple. Allows adding, retrieving and querying
Comparators Basic comparison methods used by the query logic
ContentUtils Helpers to calculate content related data, e.g. hash codes and size
Iti18RouteBuilder Route for the stored query transaction.
Iti4142RouteBuilder Routes for the register document set transactions
Iti43RouteBuilder Route for the retrieve document set transaction
QueryMatcher Matching code for various stored query types used by ITI-18.
RegRepModelExtension The DSL extension for the routes.
SearchDefinition The DSL element for creating a search query in a route
SearchProcessor The processor that performs search queries using the data store
SearchResult An enum that represents the type of results from a search query
Server The main entry point of the demo repository that starts the server

Test files:

TestRepositoryAndRegistry Basic tests that send individual requests and check their results
TestThreading A multi-threading test to show the thread-safety of the repository and of the XDS components
Task Base class for tasks used in the multi-threading test.

Configuration files:

context.xml Spring application context containing beans for Camel and IPF configuration as well as the data store.
log4j.xml, logging.properties logging configuration.

The repository can be started within Eclipse or from command line using the startup.bat after building an assembly. For this guide it is assumed that you have installed the Groovy Eclipse plugin as described in our development setup. To start the server within Eclipse, right click on Server.groovy and choose Run as/Groovy. The repository can be configured to use HTTPS by specifying the command line argument secure.

Running XDSToolKit tests against the demo repository

You can either implement your own XDS source and consumer or use the XDSToolKit to run tests against the repository. To use the toolkit you should first ensure that it runs fine against the public NIST repository that it is pre-configured with. Then you can change xdstest/actors.xml in the XDSToolKit installation to make it run against the demo repository. Add a new site to the existing <sites>:

  <site name="ipf">
    <transaction name="pr.b">http://localhost:9091/xds-iti41</transaction>
    <transaction name="r.b">http://localhost:9091/xds-iti42</transaction>
    <transaction name="sq.b">http://localhost:9091/xds-iti18</transaction>

    <transaction name="pr.b" secure="1">https://localhost:9091/xds-iti41</transaction>
    <transaction name="r.b" secure="1">https://localhost:9091/xds-iti42</transaction>
    <transaction name="sq.b" secure="1">https://localhost:9091/xds-iti18</transaction>

    <transaction name="pr.a">http://localhost:9091/xds-iti15</transaction>
    <transaction name="r.a">http://localhost:9091/xds-iti14</transaction>
    <transaction name="sq.a">http://localhost:9091/xds-iti18</transaction>

    <transaction name="pr.a" secure="1">https://localhost:9091/xds-iti15</transaction>
    <transaction name="r.a" secure="1">https://localhost:9091/xds-iti14</transaction>
    <transaction name="sq.a" secure="1">https://localhost:9091/xds-iti18</transaction>

    <repository uid="1.19.6.24.109.42.1">http://localhost:9091/xds-iti43</repository>
    <repository uid="1.19.6.24.109.42.1" secure="1">https://localhost:9091/xds-iti43</repository>
  </site>

The demo supports most of the XDS.b tests that are required for registry/repository implementations at the connectathon. Note that some of the XDS tests are checking the forwarding of registration requests to the public repository. At the moment the demo repository does not forward these requests. Instead the entries will be registered within its own registry. The following is the list of tests that we verified:

Provide and Register Document Set:
11966, 11979, 11983, 11981, 11986

Register Document Set:
11990, 11991, 11992, 12022, 11993, 11994, 11995, 11997, 11999, 12000, 12001, 12326, 12323, 12327, 12084, 12002, 12004, 11996

Retrieve Document Set Transaction:
12029, 12021, 12360

Stored Query Transaction:
11897, 11898, 11899, 11901, 11902, 11903, 11904, 11905, 11906, 11907, 11908, 11909

To run the stored query transaction tests you have to run test 12346 to load data into the repository that is used by these tests. Also, the stored query transaction tests assume that no other data has been loaded, i.e. there will be test failures if you run one of the other tests before them. Because the demo repository is not persistent you can simply restart the server to get an empty store.

Running one of these tests is very simple:

xdstest -err --s ipf -t 11966

You can also run tests via HTTPS if you have started the demo repository with the secure command line argument:

xdstest --secure -err --s ipf -t 11966

IPF XDS related code snippets

The main purpose of the demo repository is to demonstrate the features that the IPF XDS components offer. This section takes a closer look at such code pieces.

Basic configuration

Configuration of an XDS application is pretty similar to the standard configuration of an IPF application. The main difference is that we have to configure CXF. Because we run the application within a Tomcat environment, we must ensure that CXF does not start its own Jetty instance. Here is the commented context.xml:

<!-- We can add more CXF related namespaces here if we need to add something to
     the default CXF configuration. -->
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang" 
    xmlns:camel="http://activemq.apache.org/camel/schema/spring"
    xmlns:util="http://www.springframework.org/schema/util"
    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://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://activemq.apache.org/camel/schema/spring 
http://activemq.apache.org/camel/schema/spring/camel-spring.xsd
">

    <!-- The following imports are required to configure CXF. cxf-servlet
         is imported to configure CXF to run with servlet support. This 
         allows us to use Tomcat with the CXFServlet instead of using CXF 
         with a standalone Jetty server. -->
    <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" />

    <!-- Camel context and producer -->
    <camel:camelContext id="camelContext">
        <camel:jmxAgent id="agent" disabled="true" />
    </camel:camelContext>
    
    <bean id="producerTemplate" factory-bean="camelContext" factory-method="createProducerTemplate"/>

    <!-- Our route builders for the ITI transactions -->
    <bean id="iti4142RouteBuilder" depends-on="routeModelExtender"
        class="org.openehealth.ipf.tutorials.xds.Iti4142RouteBuilder">
    </bean>

    <bean id="iti43RouteBuilder" depends-on="routeModelExtender"
        class="org.openehealth.ipf.tutorials.xds.Iti43RouteBuilder">
    </bean>

    <bean id="iti18RouteBuilder" depends-on="routeModelExtender"
        class="org.openehealth.ipf.tutorials.xds.Iti18RouteBuilder">
    </bean>

    <!-- The DSL extensions. We use the core extension of the IPF and the ones 
         that come with the XDS components. In addition, we also define our own. -->
    <bean id="coreModelExtension"
        class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension">
    </bean>

    <bean id="xdsModelExtension"
        class="org.openehealth.ipf.platform.camel.ihe.xds.commons.extend.XDSModelExtension">
    </bean>
    
    <bean id="regRepModelExtension" class="org.openehealth.ipf.tutorials.xds.RegRepModelExtension">
        <property name="dataStore" ref="dataStore"/>
    </bean>

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

    <!-- The store that contains all the in-memory documents and their meta data -->    
    <bean id="dataStore" class="org.openehealth.ipf.tutorials.xds.DataStore" />
</beans>

Exposing the endpoints

To allow clients to communicate with our registry/repository, we need to define routes that automatically expose the SOAP-based endpoints. We do not define all the SOAP related details directly. A from(...) is all it takes. You can find those in the route builders. Take a look at the following snippets:

Iti18RouteBuilder.groovy
    public void configure() throws Exception {
        ...
        // Entry point for Stored Query
        from('xds-iti18:xds-iti18')
Iti4142RouteBuilder.groovy
    public void configure() throws Exception {
        ...
        // Entry point for Provide and Register Document Set
        from('xds-iti41:xds-iti41')
        ...
        // Entry point for Register Document Set
        from('xds-iti42:xds-iti42')
Iti43RouteBuilder.groovy
    public void configure() throws Exception {
        ...
        // Entry point for Retrieve Document Set
        from('xds-iti43:xds-iti43')

Validating incoming messages

Once the endpoints have been exposed, clients can send in messages. These messages might or might not conform to the IHE specification. It is usually a good idea to validate incoming messages before processing them. The XDS component offer a simple validation. This is not meant to be complete, e.g. it cannot validate that a patient ID is actually known to the registry, but it performs a variety of checks that will simplify our route implementations. All routes of the demo repository perform this basic validation step right after logging the incoming request, e.g. in ITI-43:

Iti43RouteBuilder.groovy
        from('xds-iti43:xds-iti43')
            .log(log) { 'received iti43: ' + it.in.getBody(RetrieveDocumentSet.class) }
            .validate().iti43Request()

A validation failure will throw an XDSMetaDataException. We do not need to put any onException handling in the route for this exception. The XDS components convert such validation failures into an equivalent XDS response with the correct error code. Therefore, we can also throw this exception anywhere else in our routing. E.g. the demo repository contains code to check a specific patient ID used by the XDSToolKit tests to see if we identify patient IDs that should be unknown to the registry. Because the demo does not track patients yet, we simply throw an exception if this specific patient ID is found in a request:

Iti4142RouteBuilder.groovy
        from('direct:checkPatientIds')
            .choice().when { it.in.body.req.submissionSet.patientId.id == '1111111' }
                .fail(UNKNOWN_PATIENT_ID)
            ...

A failure is reported by throwing an XDSMetaDataException via the fail DSL extension that is implemented in RegRepModelExtension.groovy. The code snippet below shows that this is simply a shortcut to throw the exception.

RegRepModelExtension.groovy
        ProcessorType.metaClass.fail = { message ->
            delegate.process { 
                throw new XDSMetaDataException(message)
            }
        }

If any other exception is thrown in the route, the XDS components will report a general error in the failure response (either XDSRepositoryError or XDSRegistryError). Of course you can use standard exception handling from Camel to handle such cases.

No matter what exception is thrown in our route, Camel - at least in versions 1.x - will try to redeliver the message by default. We do not expect that the failures will magically go away and therefore, we should disable this redelivery behavior. In all route builders you will find the following snippet at the beginning of the configure method. You can also configure the redelivery policy of the deadLetterChannel, but this way is usually sufficient:

        errorHandler(noErrorHandler())

Using the meta data classes

Once validated, we want to start processing an incoming message. The format of the data structures that we receive in the message body is very important. By default we receive instances of the raw ebXML classes. While these might be interesting for some use cases, we often want to use classes that are closer to the XDS meta classes defined by the IHE specification. These meta classes serve two purposes: They ensure conformance with the XDS specification and they are much easier to use than the more generic ebXML classes. The route builders of the demo repository all convert the ebXML bodies to the meta classes after validation. There are different ways to do this. One way is to simply use convertBodyTo which results in our body to be converted from the ebXML class to the meta data class. This is done in the ITI-43 route:

Iti43RouteBuilder.groovy
        // Entry point for Retrieve Document Set
        from('xds-iti43:xds-iti43')
            .log(log) { 'received iti43: ' + it.in.getBody(RetrieveDocumentSet.class) }
            // Validate and convert the request
            .validate().iti43Request()
            .convertBodyTo(RetrieveDocumentSet.class)

Another way is to retrieve the meta class instance via getBody. E.g. in the ITI-41 route builder, we transform the input body into a map that contains the actual request object. This allows us to access the request at any stage in our route, no matter what we were currently doing. To create the map we use a transform processing in which we fill the map:

Iti4142RouteBuilder.groovy
        from('xds-iti41:xds-iti41')
            .log(log) { 'received iti41: ' + it.in.getBody(ProvideAndRegisterDocumentSet.class) }
            // Validate and convert the request
            .validate().iti41Request()
            .transform { 
                [ 'req': it.in.getBody(ProvideAndRegisterDocumentSet.class), 'uuidMap': [:] ] 
            }

In contrast to convertBodyTo, getBody does not replace the body of the message automatically. Check out the log step at the beginning of the route. It uses getBody to retrieve the meta class. The good thing about these classes is that they have meaningful equals, hashCode and toString implementations. The logging step converts the ebXML class on-the-fly and uses its toString method to get a nice textual representation. If we use convertBodyTo instead, the validation step will fail, because it expects an ebXML class in the message body.

Lets look at some typical use cases that require access to the meta classes.

Evaluating the query type

The next code snippet shows the dispatching of an ITI-18 message based on the stored query type. We use content based routing via choice to call sub routes that perform the corresponding query logic. The queryType method is a simple shortcut to get the query type property from the request message. If non of the supported query types is found we throw an exception via the fail processor. All query types that are defined by the IHE specification are listed in the enum org.openehealth.ipf.platform.camel.ihe.xds.commons.requests.query.QueryType.

Iti18RouteBuilder.groovy
    public void configure() throws Exception {
        ...

        from('xds-iti18:xds-iti18')
            ...
            // Dispatch to the correct query implementation
            .choice()
                .when { queryType(it) == FIND_DOCUMENTS }.to('direct:findDocs')
                .when { queryType(it) == FIND_SUBMISSION_SETS }.to('direct:findSets')
                .when { queryType(it) == FIND_FOLDERS }.to('direct:findFolders')
                .when { queryType(it) == GET_SUBMISSION_SET_AND_CONTENTS }.to('direct:getSetAndContents')
                .when { queryType(it) == GET_DOCUMENTS }.to('direct:getDocs')
                .when { queryType(it) == GET_FOLDER_AND_CONTENTS }.to('direct:getFolderAndContents')
                .when { queryType(it) == GET_FOLDERS }.to('direct:getFolders')
                .when { queryType(it) == GET_SUBMISSION_SETS }.to('direct:getSets')
                .when { queryType(it) == GET_ASSOCIATIONS }.to('direct:getAssocs')                
                .when { queryType(it) == GET_DOCUMENTS_AND_ASSOCIATIONS }.to('direct:getDocsAndAssocs')
                .when { queryType(it) == GET_FOLDERS_FOR_DOCUMENT }.to('direct:getFoldersForDoc')
                .when { queryType(it) == GET_RELATED_DOCUMENTS }.to('direct:getRelatedDocs')                
                .otherwise().fail(ErrorCode.UNKNOWN_STORED_QUERY)
            .end()

         ...
    }

    def queryType(exchange) { exchange.in.body.req.query.type }
Splitting for individual entry processing

Many XDS transactions work with sets of entries, e.g. we upload or download multiple documents instead of just one. Using the splitter we can break down the request message into its individual entries and process them individually. In the demo repository this is done in many cases. The next snippet of the ITI-43 route shows how to retrieve a document set by retrieving each document from the store one at a time. Using split we perform the actual splitting of the message by taking the list of documents contained in the meta class. We tell the splitter to aggregate a result list using the retrieved documents. This list is going to be in the message body after the splitting functionality has finished processing (indicated by end()). The entries of the list are the result of the processing of retrieve, which is a custom DSL element that calls DataStore.get() to get the contents of the document. Finally we can simply transform the message using the aggregated list of documents and putting it into the meta class for the response.

Iti43RouteBuilder.groovy
            // Retrieve each requested document and aggregate them in a list
            .split { it.in.body.documents }
            .aggregate { target, next -> target.out.body.addAll(next.out.body) }
            .retrieve()            
            .end()
            // Create success response
            .transform { new RetrievedDocumentSet(Status.SUCCESS, it.in.body) }

Secure transport

Using HTTPS instead of HTTP requires very little work. In fact, for a registry/repository it does not require anything related to IPF. We simply configure Tomcat to use secure transport for our webservices. With the embedded Tomcat class of the XDS test package, this is only a few lines of code:

Server.groovy
        servletServer.secure = args.length == 1 && args[0].equals('secure')
        servletServer.keystoreFile = 'keystore'
        servletServer.keystorePass = 'changeit'
        servletServer.truststoreFile = 'keystore'
        servletServer.truststorePass = 'changeit'

The keystore provided with the test application is the one that is used in the XDSToolKit. Therefore, you can use the demo repository with secure transport enabled tests from the XDSToolKit. Note that those keystores are changed from time to time. You can replace the keystore of the demo repository by simply overwriting the keystore file.

The configuration of a complete Tomcat installation might be a little bit more but it is well documented here. Of course your mileage may vary if you are using a different container. In any case, you will not require additional configuration steps within a registry/repository implementation. Note that clients (source and consumer) will need to configure the endpoint to use HTTPS. This is not part of this guide. You can find more details in the standard documentation.

Auditing

By default auditing is turned on by all endpoints. To configure the syslog server that receives auditing messages have a look at Server.groovy:

Server.groovy
    private static final int SYSLOG_PORT = 514
    ...    
    public static void main(String[] args) {
        ...
        AuditorModuleContext.context.config.auditRepositoryHost = 'localhost'
        AuditorModuleContext.context.config.auditRepositoryPort = SYSLOG_PORT
        ...
    }

Auditing messages will always be send. Because they are send unreliably via the UDP protocol (this is the default), the XDS components "do not care" if there is actually a syslog server running at the specified host and port. If you want to see the audit messages that the demo repository logs, you can install a syslog server at localhost using the standard syslog port 514 or you can change the settings in Server.groovy to match your setup.

If you want to disable auditing you can do so by changing the endpoint configurations, e.g. for ITI-18:

Iti18RouteBuilder.groovy
        from('xds-iti18:xds-iti18?audit=false')
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.