Feb 28, 2011

Is it possible to extend the Java DSL of Apache Camel in Java?


Introduction 

This post contains my experiments to extend the Apache Camel Java DSL, also referred to as route builder fluent API. I'll try to clarify what I mean by extend with an example. With Camel (version 2.6) you are able to create a route like this:
...
from("direct:start").to("direct:end")
...
By extend the fluent API, I mean being able to add a new elements  to the existing set of API elements, e.g. beep(). Some route that uses those new route elements might look like this:
...
from("direct:start").beep().to("direct:end")
...
In the route above, the beep()element is not part of the Camel DSL. To stay focused, I will neither discuss the extensions with Groovy, Scala or other JVM languages other than Java, nor the motivation for having DLS extensions in Java. 

Extend the DSL, but how?

My experiment was driven by the idea to extend the DSL using an N-tuple of custom (MyRouteBuilderMyRouteDefinitionMy1stProcessorDefinition,  ... , MyNthProcessorDefinition), that are subclases of RouteBuilder, RouteDefintiion and ProcessorDefinition
To use the extended DSL I would subclass MyRouteBuilder and write the route just as I would write it when I subclass Camel's RouteBuilder. As you would expect MyRouteBuilder creates MyRouteDefinition, that will create My1stProcessorDefinition ... MyNthProcessorDefinition where applicable.
In this definition the beep() extension suggested above would be part of the second element of the tuple, which is MyRouteDefinition. Once again, back to the N-tuple definition; it is neither scientific nor official definition. I introduce it solely, because I believe it clearly explains what I mean. I hope it is correct, please comment if you think I am wrong.

What is the problem?
I will try to explain what is currently problematic with the extensions. Here is how a route with a beep() extension might look like, using the proposed extension technique. First I define the MyRouteBuilder:
public abstract class MyRouteBuilder extends RouteBuilder {
  public MyRouteBuilder() {
    setRouteCollection(new MyRoutesCollection());
  }
  private class MyRoutesCollection extends RoutesDefinition {
    public MyRouteDefinition from (String uri){
      // Without the MyRouteDefinition return type
      // beep will not be recognized
      return (MyRouteDefinition)super.from(uri);
    }
    //Needed by the overriden from method 
    protected MyRouteDefinition createRoute() {
      MyRouteDefinition result = new MyRouteDefinition();
      ...
    }
  }
}
Next I can define MyRouteDefinition that adds the beep() method:
import org.apache.camel.RouteDefinition;
public class MyRouteDefinition extends RouteDefinition {
   /**
    * My extension method
    */
    public MyRouteDefinition beep(){
        //beep implementation here
        ...
        return this;
    }
}
Now the use of the beep() extension in the route:
public class MyRouteBuilderTest extends MyRouteBuilder{
  @Override
  public void configure() throws Exception {
      // This compiles and runs
      from("direct:start")
       .beep().to("mock:end");
       
      from("direct:start")
       .filter(header("soundAllowed").isEqualTo("true"))
       // This does not compile
       .beep().to("mock:end");
  }
} 
The first problem is the type cast in the from(String uri) method in MyRouteBuilder. It might seem negligible at first, but I have actually overriden only one of the DSL methods in RouteBuilder. Overiding all methods would lead to duplication and inconsistency on new Camel releases.

The second more obvious problem, is that MyRouteBuilderTest does not compile. It does not compile, because the filter(...) element returns FilterDefinition, and FilterDefinition does not have any information about the existence of the beep() extension, since it is not a subclass of MyRouteDefinition.
import org.apache.camel.RouteDefinition;
public class FilterDefinition extends ExpressionNode {
   /**
    * My extension method
    */
    public MyRouteDefinition beep(){
        //beep implementation here
        ...
        return this;
    }
}

This means, that I can use my new DSL extensions only direct after from().

Conclusion

The proposed extension mechanism strategy is not applicable in Java, because the new DSL elements are "lost" when standard Camel DSL elements, like filter(...) are used.  The latter limitation is a source of confusion that might put the acceptance of such a DSL extension approach in question. I am convinced that with some changes in camel-core, extensions of this nature are possible in Java.