Appendix C - IPF Guidelines
DSL extensions guide
This section gives some guidance how to write DSL extensions for IPF. As an example we introduce a new DSL element translate for translating the in-message body from one language into another language and write the result to the out-message body. To keep the example focused, we won't discuss the actual logic for doing the translation, instead we'll focus only on the DSL part of this example. We start with the simplest case and continue to evolve the example by adding new requirements.
Processor with custom name
This is the simplest case for defining a DSL extension. Let's say we have a custom processor which translates the in-message body into a language that matches the system's default locale.
package example; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.openehealth.ipf.platform.camel.core.util.Exchanges; public class Translator implements Processor { public void process(Exchange exchange) throws Exception { String input = exchange.getIn().getBody(String.class); String output = ... // translate from input using default locale Exchanges.resultMessage(exchange).setBody(output); } }
The result is written in an exchange-pattern-sensitive manner using the Exchanges.resultMessage() method. Without defining a custom DSL element we can already use this processor in route definitions with the process DSL element.
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .process(new Translator()) .to('direct:output') } }
What we want to achieve is to make the DSL more expressive by introducing a translate DSL element. The result should look like
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .translate() .to('direct:output') } }
Apache Camel doesn't allow us to modify the classes that define the DSL (these are called model classes). To be more precise we cannot modify existing Camel model classes using Java (without hacking the Camel code itself). We therefore use Groovy meta-programming for introducting new methods to these model classes. To introduce the translate extension we have to define a translate() method on the class that matches the return type of the from() method. In our case this is org.apache.camel.model.ProcessorType (from() actually returns a type that extends ProcessorType but this is not relevant for our example). Here's the extension definition
package example import org.apache.camel.model.ProcessorType class MyExtension { static extensions = { ProcessorType.metaClass.translate = {-> delegate.process(new Translator()) } } }
The translate extension is added to the ProcessorType meta-class using a closure that implements this extension. The delegate variable is the object on which we call the translate() method. In our case this is the object returned from the from('direct:input') method call. The process(new Translator()) call is now made by the extension definition instead of made directly in the route definition. To activate this extension we use the following beans in our application context.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://activemq.apache.org/camel/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext" /> <bean id="routeBuilder" depends-on="routeModelExtender" class="example.MyRouteBuilder"> </bean> <bean id="myExtension" class="example.MyExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="myExtension" /> </list> </property> </bean> </beans>
The routeBuilder bean is configured to depend on the routeModelExtender. This ensures that the extension is activated before the route builder is used.
| Extensions and OSGi For activating extensions inside an OSGi environment refer to the extender bundles section from the OSGi support section. |
DSL extensions using a model class
In the previous section we wrote a DSL extension without defining a model class. There are some cases where it becomes necessary to use a model class e.g. if you want to lookup beans from the application context. Let's say we want to configure our translation processor with a custom dictionary that is defined as bean in the Spring application context. We also want to pass that bean name as argument to the DSL extension.
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .translate('myCustomDictionary') .to('direct:output') } }
Here, we want our DSL extension to look up the bean from the application context. We could inject an object doing a context-specific bean-lookup into our extension definition but this is highly discouraged because it causes problems (not only) in OSGi environments. The proper way is to use a separate model class for the translate DSL extension.
package example; import java.util.Map; import org.apache.camel.Processor; import org.apache.camel.model.OutputType; import org.apache.camel.spi.RouteContext; public class TranslatorType extends OutputType<TranslatorType> { private String dictionaryBeanName; public TranslatorType() { this(null); } public TranslatorType(String dictionaryBeanName) { this.dictionaryBeanName = dictionaryBeanName; } @Override public Processor createProcessor(RouteContext routeContext) throws Exception { if (dictionaryBeanName != null) { return new Translator(routeContext.lookup(dictionaryBeanName, Map.class)); } else { return new Translator(); } } }
We extend Camel's org.apache.camel.model.OutputType class to define our own custom model class and override its createProcessor(RouteContext) method. Instead of creating the processor in the DSL extension definition directly we create the processor in that method. Camel passes the current route context as argument from which we can lookup beans in the Spring application context. The name of the dictionary bean to lookup is stored in the dictionaryBeanName instance variable. For this code to work we need to add an additional constructor to our Translator class.
package example; import java.util.map; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.openehealth.ipf.platform.camel.core.util.Exchanges; public class Translator implements Processor { private Map dictionary; public Translator() { this(null); } public Translator(Map dictionary) { this.dictionary = dictionary; } public void process(Exchange exchange) throws Exception { String input = exchange.getIn().getBody(String.class); String output = ... // translate from input using default locale and custom dictionary Exchanges.resultMessage(exchange).setBody(output); } }
Finally, we use this model class in our extension definition.
package example import org.apache.camel.model.ProcessorType class MyExtension { static extensions = { ProcessorType.metaClass.translate = {-> delegate.addOutput(new TranslatorType()) return delegate } ProcessorType.metaClass.translate = {String dictionaryBeanName -> delegate.addOutput(new TranslatorType(dictionaryBeanName)) return delegate } } }
We define two translate extensions here. One without parameters and another one with a String parameter for the dictionary bean name. Our custom model class instance is added as output to the object on which the translate method is called i.e. the delegate. The delegate is then returned by the extension definition. The configuration of the route builder and the extension class in the Spring application context remains the same except that we additionally need to define a myCustomDictionary bean to make the above example route work.
Parameterized DSL extensions
In the previous section we've seen how to configure our translator with a custom dictionary. We've done this by passing the dictionary bean name as argument to the translate method. To make the DSL even more readable we'd like to have something like this.
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .translate().withDictionary('myCustomDictionary') .to('direct:output') } }
Here we introduced a withDictionary parameter to the translate extension that takes the dictionary bean name as argument (although it is technically a method we call it a parameter because it is only valid in context of the translate DSL extension). For implementing this we need to make some changes to the classes we've implemented so far. First, we have to add a withDictionary(String) method to our model class. This method replaces the constructor with the String parameter from the previous section.
package example; import java.util.Map; import org.apache.camel.Processor; import org.apache.camel.spi.RouteContext; import org.openehealth.ipf.platform.camel.core.model.DelegateType; public class TranslatorType extends DelegateType { private String dictionaryBeanName; public TranslatorType withDictionary(String dictionaryBeanName) { this.dictionaryBeanName = dictionaryBeanName; return this; } @Override protected Processor doCreateDelegate(RouteContext routeContext) { Translator translator = null; if (dictionaryBeanName != null) { translator = new Translator(routeContext.lookup(dictionaryBeanName, Map.class)); } else { translator = new Translator(); } return translator; } }
The withDictionary parameter is specific to our translate DSL extension i.e. you can only use it after calling translate() in a route definition. The return value is of type TranslatorType. If you need further parameterization of the translate DSL extension you can do so by adding further methods to the TranslatorType class. We also need to change the base class to org.openehealth.ipf.platform.camel.core.model.DelegateType and implement the abstract doCreateDelegate(RouteContext) method instead of createProcessor(RouteContext). This is needed if we want to reuse our Translator implementation, otherwise, we would have to provide an org.apache.camel.processor.DelegateProcessor implementation. The next step is to change our extension definition.
package example import org.apache.camel.model.ProcessorType class MyExtension { static extensions = { ProcessorType.metaClass.translate = {-> TranslatorType result = new TranslatorType() delegate.addOutput(result) return result } ProcessorType.metaClass.translate = {String dictionaryBeanName -> TranslatorType result = new TranslatorType(dictionaryBeanName) delegate.addOutput(result) return result } } }
The important difference is that the return type of our extension definition is our custom TranslatorType. If we'd return the delegate we would get an error when we try to invoke the withDictionary() method after the translate() method.