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
- how to write routes with the IPF scripting layer.
- how to extend the DSL with the DSL extension mechanism.
- how to configure the application components with Spring.
- how IPF uses Maven 2 to build the project, manage dependencies and package distribution bundles.
- 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.
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' } }
- 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.
- 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).
...
@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.
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.
<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.
<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.
<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:
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:
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.
<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. |