Contents
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 standalone IPF runtime section for installation and startup instructions.
Download application bundles
An archive (ipf-tutorials-osgi-<version>.zip) containing the application bundles can be downloaded from here. These bundles are part of the IPF version 2.1 tutorials. Here's a short description.
| Bundle | Description |
|---|---|
| org.openehealth.ipf.tutorials.tutorials-osgi-extension_2.2.0.SNAPSHOT.jar | Extension bundle containing tutorial-specific DSL extensions. |
| org.openehealth.ipf.tutorials.tutorials-osgi-mapping_2.2.0.SNAPSHOT.jar | Bundle containing a mapping definition for commons-map. |
| org.openehealth.ipf.tutorials.tutorials-osgi-route-file_2.2.0.SNAPSHOT.jar | HL7 message processing route that reads HL7 messages from a file endpoint. |
| org.openehealth.ipf.tutorials.tutorials-osgi-route-web_2.2.0.SNAPSHOT.jar | HL7 message processing route that reads HL7 messages from an HTTP endpoint. |
| org.openehealth.ipf.tutorials.tutorials-osgi-service_2.2.0.SNAPSHOT.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 ipf-tutorials-osgi-<version>.zip and copy
- all contained jar files (org.openehealth.ipf.tutorials.tutorials-osgi-*-2.2.0.SNAPSHOT.jar) to the $IPF_RUNTIME_HOME/bundles directory and
- the org.openehealth.ipf.tutorials.osgi.service.cfg file to the $IPF_RUNTIME_HOME/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/equinox/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 the last entry of reference:file:bundles/...).
... reference:file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-mapping_2.2.0.SNAPSHOT.jar, \ reference:file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-extension_2.2.0.SNAPSHOT.jar, \ reference:file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-service_2.2.0.SNAPSHOT.jar, \ reference:file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-route-file_2.2.0.SNAPSHOT.jar, \ reference:file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-route-web_2.2.0.SNAPSHOT.jar
Don't forget to append a comma-blank-backslash character sequence after this last bundle entry reference:file:bundles/..., 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:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-mapping_2.2.0.SNAPSHOT.jar install file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-extension_2.2.0.SNAPSHOT.jar install file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-service_2.2.0.SNAPSHOT.jar install file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-route-file_2.2.0.SNAPSHOT.jar install file:bundles/org.openehealth.ipf.tutorials.tutorials-osgi-route-web_2.2.0.SNAPSHOT.jar
The result of each command is the id of the newly installed bundle. Here's an example
After typing ss you should again see the same list of installed bundles as in the previous section.
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=98). The extensions are recognized by the osgi-extender-basic bundle. After typing
start 99
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 100
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 101
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 102
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 bundle tutorial-osgi-mapping. Refer to the description of the commons-map 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
- 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 the HL7 message in the Body field
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 provides a code mapping file that is recognized the by the mapping service. For details how to contribute custom mappings to commons-map refer to the description of the commons-map bundle in the OSGi support section. Here's the simple mapping definition
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 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.
import org.apache.camel.Exchange import org.apache.camel.model.ProcessorDefinition class SampleExtension { static extensions = { ProcessorDefinition.metaClass.setFilename = {String filename -> delegate.setHeader(Exchange.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.
... 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.
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.
<?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.
<?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.
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.
<?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.
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.
<?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.
<?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).
<?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.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel-osgi="http://camel.apache.org/schema/osgi" xmlns:camel-spring="http://camel.apache.org/schema/spring" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/osgi http://camel.apache.org/schema/osgi/camel-osgi.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:annotation-config /> <camel-osgi:camelContext id="camelContext"> <camel-spring:routeBuilder ref="routeBuilder"/> </camel-osgi: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.
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.
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.
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() } }
