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 standard HTTP POST and GET requests.
The steps of the tutorial are:
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
The Maven project is created via an IPF maven archetype. Find a suitable location on your disk and create the project using the following command (make sure that everything 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=2.2.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 and CXF. 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>2.2.0</version>
</dependency>
<dependency>
<groupId>org.openehealth.ipf.platform-camel</groupId>
<artifactId>platform-camel-lbs-http</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<scope>test</scope>
<version>2.2.8</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. This must be changed in order 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. Add the extension of the LBS by adding another bean:
...
<!-- 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:
...
<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>
Add beans for a ResourceFactory and a LargeBinaryStore. The factory creates resources for the images that are stored in the large binary store while they are being processed by the router:
...
<!-- 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>
Beginning with IPF 2.2.0 it is necessary to use a modified version of the Jetty component with the LBS. This component ensures that the LBS functionality has access to the pure InputStream despite various changes done in Camel 2.3.0. To enable the usage of this component you need to add the following bean:
...
<bean id="jetty" class="org.openehealth.ipf.platform.camel.lbs.http.LbsJettyHttpComponent" />
Create the webservice
The webservice is created using a wsdl-first approach. It should allow uploading and downloading of images via two methods. The wsdl defines a service with these two methods and a type for the image. The upload method returns an ID for an image, that is used is subsequent calls to download the image again.
Create the following wsdl-file in the directory src/main/resources/wsdl and name it 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 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:
...
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>2.2.6</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 run Maven in the project root directory on the command line:
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. Exit 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 the new class 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. For downloading, 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:
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: Exceptions are not properly handled in this code to keep it as small as possible.
Add a class called ImageBinServer to start and stop the CXF service. The class should be in the main java sources and in the org.openehealth.tutorial.imagebin package:
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 to this (using helper classes from the org.apache.commons.io package):
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
The next step is to expose the webservice via an HTTP endpoint. To do this create a route via Camel. The route connects the HTTP endpoint with the CXF endpoint of the 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.
...
<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 a few CXF-related Camel resources are imported that define the cxf namespace. Make sure that the beans tag of the context.xml file looks like the following code:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:camel="http://camel.apache.org/schema/spring" xmlns:cxf="http://camel.apache.org/schema/cxf" 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://camel.apache.org/schema/cxf http://camel.apache.org/schema/cxf/camel-cxf.xsd http://camel.apache.org/schema/spring http://camel.apache.org/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 are used in the route for handling HTTP and CXF resources:
...
<!-- 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>
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:
package org.openehealth.tutorial import static org.apache.camel.component.cxf.CxfConstants.* import static org.apache.camel.Exchange.* import javax.activation.DataHandler import javax.activation.DataSource import org.apache.camel.spring.SpringRouteBuilder import org.apache.cxf.message.MessageContentsList import org.openehealth.ipf.platform.camel.lbs.http.process.ResourceList class SampleRouteBuilder extends SpringRouteBuilder { void configure() { // 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:8412/imagebin') .choice() .when(header(HTTP_METHOD).isEqualTo('POST')).to('direct:upload') .when(header(HTTP_METHOD).isEqualTo('GET')).to('direct:download') .otherwise().end() // Handle uploads from('direct:upload') .store().with('resourceHandlers') // ensure we can upload large files .transform { // transform the message into a CXF call it.in.headers = [(OPERATION_NAME): 'upload'] // operation [new DataHandler(it.in.body[0])] // parameters } .to('cxf:bean:imageBinServer') // webservice.upload() call .transform { it.in.body[0] } // back to http using result param 0 // Handle downloads from('direct:download') .transform { // transform the message into a CXF call it.out.headers = [(OPERATION_NAME): 'download'] // operation [it.in.headers.handle] // parameters } .to('cxf:bean:imageBinServer') // webservice.download() call .store().with('resourceHandlers') // ensure we can download large files .transform { it.in.body[0].dataSource } // back to http using data source in param 0 } }
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 the image is stored in the temporary store as soon as the HTTP POST message is identified. In the download part this is done after the webservice has been called. The two calls to the store processor use the resource handlers that are configured in context.xml.
To add a unit test for these routes, open SampleRouteTest.java in src/test/java/org/openehealth/tutorial. Remove the methods in the existing class. They test the sample route that was removed earlier. The Apache HttpClient is used to send a real test message to the route:
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:8412/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 assertEquals(200, 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:8412/imagebin"); get.setQueryString("handle=" + handle); assertEquals(200, 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.