Sunday 30 October 2011

Apache Camel: Plain Java deployment, Spring, CXF WS client and WS-Security

I needed to create an application that polled one non-standard web-service for changes and sent the results through to another web-service, an ideal use-case for Apache Camel. However I found many configuration issues that made the process a little tricky, I'll track my steps here for future reference.

I had the following requirements for my Camel deployment:

  • externalised Spring properties configuration 
  • a custom component to poll a non-standard web service (I will document this process later) 
  • using CXF to call a web service with WS-Security (username/password to start with) 
  • an plain-java uber-jar for deployment 


I use the Spring properties format to specify properties in the camel-context.xml file (rather than the Camel properties), however I did not want to specify the properties file in the camel-context.xml file (it changes on the deployment machine): this does not fit well with the camel Main method that expects to load everything using the setApplicationContextUri() method. This meant I needed to create a normal Spring application context first, load the properties, refresh the context, assign it to Camel and then set the Camel Main method to start:
public class ChangePublisherMain {
    public static final String[] CAMEL_CONTEXT_PATHS = {"META-INF/spring/camel-context.xml"};

    public static void main(String[] args) throws Exception {

        String propertiesFileLocation = args[0]

        //the Camel start for a Java JAR
        Main main = new Main();

        //create a spring context which we will populate with properties file
        AbstractApplicationContext context = new ClassPathXmlApplicationContext(CAMEL_CONTEXT_PATHS,false);
        PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
        cfg.setLocation(new FileSystemResource(propertiesFileLocation));
        context.addBeanFactoryPostProcessor(cfg);
        context.refresh();

        main.setApplicationContext(context);

        //we want to control exactly when routes are started (after we have properly configuring everything!)
        SpringCamelContext camel = (SpringCamelContext)context.getBean("camelContext");
        camel.start();
        for (RouteDefinition route : camel.getRouteDefinitions()) {
            camel.startRoute(route);
        }

        //block until the application is stopped
        main.enableHangupSupport();
        main.run();

    }
}
(note that the properties is a file system reference passed in as an argument).

I found that the Spring camelContext autoStart was giving me trouble when doing this so I had to disable to autostart in the camel-context file:
    

    
        integration.routes
        
    
This shows where the above camelContext is started. Furthermore, the routes wouldn't start automatically so the main method iterates over each route to start it within the context.

Rather than use POJO's with CXF and Camel I wanted to pass an XML Document directly, using the payload data-type that CXF should use to call the external web service. The external web service I was calling also required WS-Security with a simple username/password token. In the camel-context.xml file I created the following cxf endpoint:
    
        
            
            
                
                    
                        
                        
                        
                        
                    
                
            
        
    
This required a password callback handler, so I created something very simple in Java:
public class WSPasswordCallbackHandler implements CallbackHandler {

    private String username;
    private String password;

    public WSPasswordCallbackHandler(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public WSPasswordCallbackHandler() {
        //string constructor
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPassword() {
        return password;
    }

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

       WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

       if (pc.getIdentifier().equals(username)) {
          pc.setPassword(password);
       }
    }

}
I then created an instance of this callback handler in camel-context.xml - note that it was included as a additional WSS4JOutInterceptor in the CXF end point.
    
        
        
    
I was then able to use the end point in my Java route (notice the PAYLOAD data format):
                    to("cxf:bean:wsPublishEndpoint?" +
                        "defaultOperationName=wsSendOperation&" +
                        "defaultOperationNamespace=http://www.tempuri.co.nz/wsService&" +
                        "wrappedStyle=true&" +
                        "loggingFeatureEnabled=true&" +
                        "dataFormat=PAYLOAD");

Now the final issue was creating an 'uber-jar' for simple deployment - CXF made this very difficult as many of the spring configurations would get overwritten. Using the Maven Shade plugin with carefully selected transformers allowed the conflicting files to be aggregated:
            
                org.apache.maven.plugins
                maven-shade-plugin
                1.4
                
                    
                        package
                        
                            shade
                        
                        
                            
                                
                                    ChangePublisherMain
                                
                                
                                    META-INF/services/org/apache/camel/TypeConverter
                                
                                
                                    META-INF/spring.handlers
                                
                                
                                    META-INF/spring.schemas
                                
                                
                                    META-INF/cxf/cxf.extension
                                
                                
                                    META-INF/extensions.xml
                                
                                
                                    META-INF/cxf/extensions.xml
                                
                                
                                    META-INF/cxf/bus-extensions.txt
                                
                                
                                    META-INF/cxf/bus-extensions.xml
                                
                                
                                    META-INF/wsdl.plugin.xml
                                
                                
                                    META-INF/tools.service.validator.xml
                                
                                
                                    META-INF/cxf/java2wsbeans.xml
                                
                                
                            
                            executable
                            true
                        
                    
                
                
                    
                        org.apache.cxf
                        cxf-buildtools
                        2.2.12
                        jar
                        compile
                    
                
            
I suspect there might be other Camel Spring resources that conflict besides the TypeConverter but I haven't found any yet. This meant I could run my java 'uber-jar' straight off the command line using java -jar ChangePublisher-1.0-executable.jar

And finally, all of my Maven dependencies:
    
        
            org.apache.camel
            camel-core
            2.8.2
        
        
            org.slf4j
            slf4j-log4j12
            1.6.1
        
        
            log4j
            log4j
            1.2.16
        
        
            org.apache.camel
            camel-cxf
            2.8.2
        
        
            org.apache.camel
            camel-spring
            2.8.2
        
        
            org.apache.cxf
            cxf-rt-ws-security
            2.4.3
        
    
Hopefully I've managed to clamber up the Apache Camel learning curve now!