Custom Code in Apache Camel
Creating custom processors, Kamelets and components
In this blog, we investigate how you can add your own custom Java code in Apache Camel. We do this step-by-step. First creating routes with expressions and processors and then a Kamelet and a custom component.
The blog assumes you have basic knowledge of Camel. It’s not so much a tutorial, but more to show what is possible.
Why do we need custom Java code?
Before we start coding, I like to say that it’s generally better to avoid custom code in Apache Camel. Why? Because the framework comes out of the box with more than 300 components. Together with integrations patterns, most integration problems can be solved.
All these components are built-in and battle-tested, so sticking with proven technology is often, the boring, but best option. This is also a lesson from experience. Often when integrations aren’t working, it has two common causes:
- External systems aren’t working.
- Custom code has bugs.
That said, sometimes you just can’t find a good component or too much route steps are needed to do something simple. In this case Camel offers multiple ways to add your own code. At the end, Camel is just Java.
The use case
Our use case is to write some code that converts files between various data formats like XML, JSON, CSV and YAML.
Can’t this be done by the Camel framework? Yes, for example in Camel 2 there was a xmljson component that converts from XML to Json and vice versa. This component is however not available anymore. So you need other ways to do it in Camel 3. For example, explained in this StackOverflow article.
As is mostly the case with Camel, there are many ways to do the same task. There is even a no code option to perform this task by using Atlas Map. But in our use case, we just want simple data format conversions. For this we use the Java library DocConverter.
The code
Step-By-Step way to build a processor
Let’s start with a very simple route:
from("file:data/in")
.to("file:data/out")
With this route you can easily move a file from one directory to another. For example this XML file:
<waffles>
<waffle>
<name>Belgian Waffles</name>
<price>$5.95</price>
<calories>650</calories>
</waffle>
<waffle>
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<calories>900</calories>
</waffle>
</waffles>
Most of the time, we also want to perform an action on the data. For example, change the name from “waffles” to the French “gaufres”. We don’t really need to write a lot of code for that and can just add a Simple expression.
from("file:data/in")
.transform(simple( "${body.replace('waffles', 'gaufres')}"))
.to("file:data/out")
A lot of actions can be done with just a bit of Simple or Groovy code. In our use case we need to go a step further, because we want to call the Java class ‘DocConverter’. This can be done by adding an inline processor.
from("file:data/in")
.process(exchange -> {
String convertedBody= DocConverter.convertXmlToJson(exchange.getBody());
exchange.getIn().setBody(convertedBody);
})
.to("file:data/out")
Inline processors are a nice way to quickly add some code to your route without the need for creating a separate class. When the code gets longer and more complex it’s better to separate the processor into its own class:
public class DocConverterProcessor implements Processor {
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn(); String body = in.getBody(String.class);
String convertedBody = DocConverter.convertXmlToJson(body);
in.setBody(convertedBody); }}
Now you can call your processor from the route:
from("file:data/in")
.process(DocConverterProcessor.class)
.to("file:data/out")
So far, we can only convert from XML to JSON. What when we want to convert to YAML instead? Do we need a separate processor? This is not needed. We can for example set a header first.
from("file:data/in")
.setHeader("convert","xml2yaml")
.process(DocConverterProcessor.class)
.to("file:data/out")
And change our processor accordingly:
public class DocConverterProcessor implements Processor {
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn(); String body = in.getBody(String.class);
String convertedBody = “”;
String convert = in.getHeader(“convert”).toString(); if(convert.equals(“xml2json”)) {
convertedBody = DocConverter.convertXmlToJson(body);
}elseif(convert.equals(“xml2yaml”)){
convertedBody = DocConverter.convertXmlToYaml(body);
}else{
convertBody = body;
}
in.setBody(convertedBody); }}
Kamelet
Kamelets allow us to call a predefined route, including custom code, as a oneliner. To do this we must create a route template:
public class MyRouteTemplates extends RouteBuilder {@Override
public void configure() throws Exception {
// create a route template with the given name
routeTemplate("docconverter")
.templateParameter("source")
.templateParameter("target")
.from("direct:docconverter")
.setHeader("convert", {{source}}2{{target}})
.process(DocConverterProcessor.class)
}
}
And then our route can be:
from("file:data/in")
.to("kamelet:docconverter?source=xml&target=json")
.to("file:data/out")
Component
It’s also possible to turn our processor into a full-fledge component. Now we can provide all kind of conversions in a single component that is easily to use within any Camel route.
For the custom component, we need three classes:
- Processor
- Endpoint
- Component
Processor
public class DocConverterProcessor implements Processor {
private DocConverterEndpoint endpoint;
private String convertedBody;
public DocConverterProcessor(DocConverterEndpoint endpoint) {
this.endpoint = endpoint;
}
@Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
String body = in.getBody(String.class);
String uri = endpoint.getUriPath().toLowerCase();
String source2target = uri.replace("docconverter://","");
switch(source2target)
{
case "xml2json":
convertedBody = DocConverter.convertXmlToJson(body);
break;
case "xml2yaml":
convertedBody = DocConverter.convertXmlToYaml(body);
break;
case "xml2csv":
convertedBody = DocConverter.convertXmlToCsv(body);
break;
case "json2xml":
convertedBody = DocConverter.convertJsonToXml(body);
break;
case "json2yaml":
convertedBody = DocConverter.convertJsonToYaml(body);
break;
case "json2csv":
convertedBody = DocConverter.convertJsonToCsv(body);
break;
case "yaml2xml":
convertedBody = DocConverter.convertYamlToXml(body);
break;
case "yaml2json":
convertedBody = DocConverter.convertYamlToJson(body);
break;
case "yaml2csv":
convertedBody = DocConverter.convertYamlToCsv(body);
break;
case "csv2xml":
convertedBody = DocConverter.convertCsvToXml(body);
break;
case "csv2json":
convertedBody = DocConverter.convertCsvToJson(body);
break;
case "csv2yaml":
convertedBody = DocConverter.convertCsvToYaml(body);
break;
default:
convertedBody = body;
}
in.setBody(convertedBody);
}
}
Endpoint
@UriEndpoint(
firstVersion = "3.14.4",
scheme = "docconverter",
title = "DocConverter Component",
syntax = "docconverter:source2target",
producerOnly = true,
category = { Category.TRANSFORMATION }
)
public class DocConverterEndpoint extends ProcessorEndpoint {
@UriPath
@Metadata(required = true)
private String uri;
private DocConverterComponent component;
public DocConverterEndpoint(DocConverterComponent component, String uri) {
super(uri,component);
this.component = component;
setUriPath(uri);
}
@Override
protected Processor createProcessor() {
return new DocConverterProcessor(this);
}
@Override
public Component getComponent(){
return (Component) component;
}
@ManagedAttribute(description = "Type of conversion")
public String getUriPath() {
return uri;
}
/**
* Type of conversion: source2target
* Source consist of (xml, json, yaml, csv) and target (xml, json, yaml,csv)
* For example xml2json
*
* @param uri The type of conversion: source2target
*/
public void setUriPath(String uri) {
this.uri = uri;
}
}
Component
public class DocConverterComponent extends DefaultComponent {
@Override
protected Endpoint createEndpoint(String uri, String context, Map<String, Object> parameters) {
DocConverterEndpoint endpoint = new DocConverterEndpoint(this, uri);
return endpoint;
}
}
These class files are gathered in a normal Java project called ‘docconverter’. Besides these classes also the following directory is created:
docconverter\src\main\resources\META-INF\services\org\apache\camel\component
This contains the file ‘docconverter’:
class=org.assimbly.docconverter.DocConverterComponent
This last file and directory are needed so that Camel automatically recognizes the component. After building the project you can now use it within any Camel project:
<dependency> <groupId>com.myorg</groupId> <artifactId>docconverter</artifactId> <version>1.0.0</version></dependency>
You can create a new route like this:
from("file:data/in")
.to("docconverter:xml2json")
.to("file:data/out")
Our output of the waffles xml:
{"waffles": {"waffle": [
{
"price": "$5.95",
"name": "Belgian Waffles",
"calories": 650
},
{
"price": "$7.95",
"name": "Strawberry Belgian Waffles",
"calories": 900
}
]}}
Examples
You can find the code of the DocConverter component here:
Besides the custom Camel component, this repo also has examples for processors and other integration patterns.
For a more detailed look on custom components, you can always check the official Camel code with all implemented Camel components:
Conclusion
Though not always needed within Camel, it’s easy to add your own code through expressions and processors. And when one really wants to make the custom code easy for other developers you can wrap it in a Kamelet or component.