Sunday, April 29, 2012

Camel and beans I

The Camel DSL is very powerful and has different dialects. You can do almost anything when defining a route nesting your business logic directly between the from()...to() statement. But is this what you want to do?

The answer is clearly no unless you apply trivial operations that let's say for the readability reason can be set directly in the route. When you are designing a system you would like to have reusable components implementing atomic business steps that can be easily tested. This makes even more sense if a team is writing the code. you can split the tasks, glue the results together and deliver the system for the user acceptance phase.

Camel has a very handy component called (how fancy) the Bean Component that you can use to modify your Exchange and even send it to endpoints. The steps to create a bean that you can use in Camel are:
  1. Identify the specific job the bean implements
  2. Implement the method(s) within a class
  3. Register the bean in the Camel context
  4. Create tests for the bean
  5. Use the bean within a route
Let's walk through these steps:

1. Some one in your organization want to add an id and a creation date to each order received and then precess the request. The order is a text file that is put in a given directory and the stage of the processing is the order initialization step. 

2. To make you life easy reuse the project created in this post and create a class called OrderHandler as shown below:


This is the implementation of the initialize method:

public class OrderHandler {


private Logger log = Logger.getLogger(OrderHandler.class);

public File initialize(@Body InputStream order) throws IOException {

File destination = FileUtil.createTempFile("init", "tmp");
log.info(destination.getAbsolutePath());

FileUtils.copyInputStreamToFile(order, destination);

Collection<String> lines = new LinkedList<String>();
String orderId = String.valueOf((new Date()).getTime());
lines.add("\n");
lines.add("orderId: " + orderId);
String date = Calendar.getInstance().getTime().toString();
lines.add("date:    " + date);

FileUtils.writeLines(destination, lines , true);

log.debug("business logic executed");
return destination;
}
}

I am using the Apache commons-io library because I don't want to reinvent the wheel especially in I/O. Note the difference between FileUtils (commons-io) and FileUtil (camel). The latter creates a convenient temp file for you in a very convenient way. Inspect the destination variable while debugging the test (point 4) for details. To keep the code focused on the important details I am actually creating an order out of a new date. Although it is a unique value it is not the best thing to do, perhaps a database would be handy here but I want to you focus on the Camel side.

Notice the usage of @Body that at run time will inject the in part of the exchange as an InputStream, the file that has been received from the producer.

3. In order to register the bean in Camel just open your camel-context.xml file and add the bean definition. That's it!


4. Before adding the bean to the route it is always a good idea to create a test for it. This is the test code:

public class OrderHandlerTest {


@Test
public void testInitialize() {
File inFile = new File("target/test-classes/testFile.txt");

InputStream is = null;

try {
is = new FileInputStream(inFile);
OrderHandler oh = new OrderHandler();
File f = oh.initialize(is);

assertNotNull(f);

} catch(Exception e) {
fail(e.toString());
}
}
}

Run the test and check the result that JUnit provides.

5. We are now nesting the bean in the route and it will be very easy as shown below:

public class RouteWithBeans extends RouteBuilder {

/*
* (non-Javadoc)
* @see org.apache.camel.builder.RouteBuilder#configure()
*/
@Override
public void configure() throws Exception {

if (getContext().hasComponent("properties") == null) {
PropertiesComponent pc = new PropertiesComponent();
pc.setLocation("classpath:META-INF/camel.properties");
getContext().addComponent("properties", pc);
}

from("{{in.dir}}").beanRef("orderHandler", "initialize").to(
"{{out.dir}}/?fileName=order.txt");

}


}

Yes, that's it! Rather than implementing a Processor directly in the route or in separate class you can use this approach. Think about it: imagine that you have a service or a module somewhere implementing a big chunk of business logic. If you use a bean you can add a well tested and consistent code reusing what has already been created.
Note that /?fileName=order.txt will be the file name once the order is enriched and moved. That should be revised since you want to have a specific file name for each order. You can use a Simple expression in the File component for that.

You can now run the route within Eclipse directly as shown here. While details about the usage of {{}} the Properties component are here.

You can get the code form github.

No comments:

Post a Comment