tisdag 22 mars 2011

Templating webservices with Velocity using Camel

When creating webservices a lot of people has objections against using JAXB for binding XML to objects. This may be because of performance reasons, allergy to generated code or simply a philosophical belief that you should not mix document centric services with an object oriented model. Whatever the reasons for not using JAXB you have I'll try to describe a possible other solution using Camel.

I created this laboration after viewing Adrian Trenamans excellent talk about CXF usage in camel, so I can and will not take credit for anything you'll see. In that talk he showed a little trick were you could use Velocity as the templating engine for generating your soap responses, without ever doing any marshalling or parsing of the XML documents.

His complete session could be found here, but do note that you need a registration at fusesource.com to see it. It is free, so if you're interested I urge you to check it out.


Defining our contract


As mostly all SOA architects would tell you should always try to start by defining a wsdl, creating the service top-down. Our intended service in this sample is going to be a very simple credit check service that accepts a customer and provides a credit score and a detailed description about the reasoning behind the score.

Our service only has one operation for now, called PerformCreditCheck:



The response is a simple message containing the Score as well as a Details field:



The Camel context


I've chosen to implement the route using the Java DSL, so after setting up the CXF endpoint in the camel context I point the package scan to the java package where my route is. The complete camel-context.xml is as follows:
<?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:cxf="http://camel.apache.org/schema/cxf"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
http://camel.apache.org/schema/cxf http://camel.apache.org/schema/cxf/camel-cxf.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"/>

<cxf:cxfEndpoint xmlns:c="http://billy.laborations/CreditCheckService/" id="creditCheckEndpoint"
address="http://localhost:9000/creditcheck"
wsdlURL="META-INF/wsdl/CreditCheckService.wsdl"
serviceName="c:CreditCheckService"
endpointName="c:CreditCheckServiceSOAP"
serviceClass="billy.laborations.creditcheck.DummyService">
<cxf:properties>
<entry key="schema-validation-enabled" value="true" />
</cxf:properties>
</cxf:cxfEndpoint>

<camelContext xmlns="http://camel.apache.org/schema/spring">
<package>billy.laborations.creditcheck</package>
</camelContext>

</beans>



As you can see I'm providing the wsdl for the service, its endpoint address as well as an implementation class.

The JAX-WS Endpoint


Looking at this class is where things are starting to get interesting. Since I don't want to deal with JAXB I chose the JAX-WS Provider type of service which accepts arbitrary messages. Any type of message that hits this service will kick of the invoke() method, leaving XML parsing etc up to the developer. If you look at the method however, you'll notice that it is set up to always throw an exception, how could it possibly work? Well, it seems that Camel hijacks this method and kicks of the route instead, but this class still needs to be existing in order for the CXF runtime to initialize.


package billy.laborations.creditcheck;

import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Provider;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.WebServiceProvider;

@WebServiceProvider(portName="CreditCheckPort", serviceName="CreditCheckService", targetNamespace="http://billy.laborations/CreditCheckService/")
@ServiceMode(Mode.PAYLOAD)
public class DummyService implements Provider<StreamSource> {

public StreamSource invoke(StreamSource request) {
throw new UnsupportedOperationException("Not implemented yet.");
}
}



Routing


The route is implemented as:


package billy.laborations.creditcheck;

import javax.xml.transform.stream.StreamSource;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.builder.xml.Namespaces;
import org.apache.cxf.message.MessageContentsList;


public class CreditCheckRoute extends RouteBuilder {

public void configure() {
// lets define the namespaces we'll need in our filters
Namespaces ns = new Namespaces("c", "http://billy.laborations/CreditCheckService/");

from("cxf:bean:creditCheckEndpoint").routeId("CreditCheckRoute")
.process(new Processor(){
public void process(Exchange exchange) throws Exception {

//Cxf stores requests in a MessageContentList. The actual message is the first entry
MessageContentsList messageList = (MessageContentsList)exchange.getIn().getBody();
exchange.getIn().setBody(messageList.get(0), String.class);
}
})
.wireTap("seda:audit")
.setHeader("FirstName", xpath("/c:Customer/FirstName").resultType(String.class).namespaces(ns))
.setHeader("LastName", xpath("/c:Customer/LastName").resultType(String.class).namespaces(ns))
.setHeader("SocialNr", xpath("/c:Customer/SocialNr").resultType(Integer.class).namespaces(ns))
.process(new CreditCheckCalculator())
.to("velocity:META-INF/velocity/creditCheckResponse.vm")
.convertBodyTo(StreamSource.class)
;


from("seda:audit")
.inOnly()
.to("log:audit")
.to("file:target/audit/?fileName=${date:now:yyyyMMdd}.txt&fileExist=Append");
}
}



As you can see the route uses XPath to set some headers based on the incoming values. This is done so that our service (CreditCheckCalculator) has easy access to them.

The business service


I've kept the service simple in this case, but the interesting part is that it's using the camel headers for getting and setting the data. You don't have to implement the service as a camel Processor, there are a lot of ways to call i.e. an existing POJO using bean referencing, but I'll leave that for now.


package billy.laborations.creditcheck;

import org.apache.camel.Exchange;

public class CreditCheckCalculator implements org.apache.camel.Processor{

public void process(Exchange exchange) throws Exception {
String firstName = (String)exchange.getIn().getHeader("FirstName");
String lastName = (String)exchange.getIn().getHeader("LastName");
int socialNr = (Integer)exchange.getIn().getHeader("SocialNr");

int creditScore;
String details;

if(firstName.equalsIgnoreCase("Billy")){
creditScore = 99;
details = firstName + " is a really good customer!";
}else{
creditScore = 1;
details = firstName + " should not be trusted!";
}

exchange.getIn().setHeader("CreditScore", creditScore);
exchange.getIn().setHeader("Details", details);
}
}



Generating the response


Why are the headers a good place to store the result then? Well, in our case it's because we have easy access to them from the velocity template:


<CreditScore xmlns="http://billy.laborations/CreditCheckService/">
<Score>$headers.CreditScore</Score>
<Details>$headers.Details</Details>
</CreditScore>


The trick here is that we're templating our response without touching any XML API's. The velocity template will simply return a String object which we can then transform to a StreamSource before handing it back to the JAX-WS endpoint.

Final thoughts


Well, that sums up my experiment. It might look like a complex setup for a very simple service, but the benefits of using templating are:

* Performance - No need to parse the request or response messages
* WS stack - Exposing the route as an CXF endpoints gives us all the power of soap handlers and CXF injectors for i.e. adding security etc.
* Adding functionality to the route - The route can easily be extended to support other EIP patterns, such as custom auditing implemented by the wiretap in our example.

If you are interested in the code, I'll make sure to upload it. Otherwise thank you for this time!

1 kommentar:

  1. We have used a lot of camel based xml services but never tried velocity with it. Interesting to see such use of velocity and camel can be powerful too. Thanks for sharing.

    SvaraRadera