Reusing routing logic through template routes
One of the key advantages of using Java for defining routes is the ability to define the same, or similar, routing logic multiple times in your integrations, while changing key elements.
Consider the case of a route that:
- Consumes bulk order data from CSV files in an input directory
- Splits it into individual orders
- Extracts the date of each order, formatted specific to the country, and converts it to a universal one
- Places an order confirmation into another directory
Now consider that you may have orders from dozens of different countries, with different order and confirmation directories, and different date formats.
You could write similar routes dozens of times, but that is going to create a maintenance problem. Alternatively, using Camel's Java DSL, you can write the common routing logic once, and then use dependency injection to vary the values that are different when you instantiate the route.
This recipe will show you a strategy for creating Camel routes that can be created with different values at runtime, parameterizing your common routing logic.
Getting ready
Define your route within a RouteBuilder
as usual, only this time make the start and end URIs, as well as any beans involved in the processing, properties of the RouteBuilder
class:
public class OrderProcessingRouteBuilder extends RouteBuilder { String inputUri; String outputUri; private OrderFileNameProcessor orderFileNameProcessor; @Override public void configure() throws Exception { from(inputUri) // split into individual lines .split(body(String.class).tokenize("\n")) .process(orderFileNameProcessor) .log("Writing file: ${header.CamelFileName}") .to(outputUri) .end(); } }
Note
Note that the URI variables are defined as package scoped. This will help us to test the class later.
The Java code for this recipe is located in the org.camelcookbook.structuringroutes.templating
package. The Spring XML files are located under src/main/resources/META-INF/spring
and prefixed with templating
.
How to do it...
Use property setters on your RouteBuilder
implementation to instantiate multiple instances of the route with different property values injected.
- Add setters for the properties:
public void setInputDirectory(String inputDirectory) { inputUri = "file://" + inputDirectory; } public void setOutputDirectory(String outputDirectory) { outputUri = "file://" + outputDirectory; } public void setOrderFileNameProcessor( OrderFileNameProcessor orderFileNameProcessor) { this.orderFileNameProcessor = orderFileNameProcessor; }
Tip
A useful trick is to construct the endpoint URIs within the setters. This way, when instantiating the
RouteBuilder
, you only have to worry about which directories to use, not about the various additional attributes that you want to repeat for the file component each time. - Validate that the mandatory bean properties were set. The following code uses the
org.apache.commons.lang.Validate
class to check for nulls and empty Strings:@PostConstruct public void checkMandatoryProperties() { Validate.notEmpty(inputUri, "inputUri is empty"); Validate.notEmpty(outputUri, "outputUri is empty"); Validate.notNull(orderFileNameProcessor, "orderFileNameProcessor is null"); }
Tip
If you are using the
RouteBuilder
from Spring, add a@PostConstruct
method to check that all of the properties have been set. This way, if all of the fields have not been initialized correctly, the application will refuse to start up. - To complete the integration we need to add a
Processor
that parses dates from a line of CSV text, changes the date to a universal format, and sets a header with the output filename. We encapsulate this logic in a class whose instances vary by a date format that is injected. The source for this class is available in the example code under:org.camelcookbook.structuringroutes.templating.OrderFileNameProcessor
. - In your Spring XML file, you can now create multiple instances of this class:
<bean id="dateFirstOrderFileNameProcessor" class="org.camelcookbook.structuringroutes.templating.OrderFileNameProcessor"> <property name="countryDateFormat" value="dd-MM-yyyy"/> </bean> <bean id="monthFirstOrderFileNameProcessor" class="org.camelcookbook.structuringroutes.templating.OrderFileNameProcessor"> <property name="countryDateFormat" value="MM-dd-yyyy"/> </bean>
- We now have all of the pieces in place to perform the same integration for a number of countries that use different input and output directories, and date formats, within their order files. We can now go ahead and instantiate the
RouteBuilder
s and inject them into the Camel context:<bean id="ukOrdersRouteBuilder" class="org.camelcookbook.structuringroutes.templating.OrderProcessingRouteBuilder"> <property name="inputDirectory" value="/orders/in/UK"/> <property name="outputDirectory" value="/orders/out/UK"/> <property name="orderFileNameProcessor" ref="dateFirstOrderFileNameProcessor"/> </bean> <bean id="usOrdersRouteBuilder" class="org.camelcookbook.structuringroutes.templating.OrderProcessingRouteBuilder"> <property name="inputDirectory" value="/orders/in/US"/> <property name="outputDirectory" value="/orders/out/US"/> <property name="orderFileNameProcessor" ref="monthFirstOrderFileNameProcessor"/> </bean> <camelContext xmlns="http://camel.apache.org/schema/spring"> <routeBuilder ref="ukOrdersRouteBuilder"/> <routeBuilder ref="usOrdersRouteBuilder"/> </camelContext>
How it works...
By treating our RouteBuilder
implementation as just another bean to use in a Spring context, we were able to instantiate it multiple times, introducing varying behavior by changing the injected values. In the future, if we were to change the routing logic, perhaps by adding more logging, it would all be done in one place in the code.
When we defined our URI properties in the RouteBuilder
, we set them as package scoped. This is a handy strategy that allows us to inject endpoint types from within the same package that are not file:
endpoints, which are set when our public setter methods are used. Since test classes are typically co-located in the same package, this allows us to initialize our RouteBuilder
with more easily testable endpoints:
OrderFileNameProcessor processor = new OrderFileNameProcessor(); processor.setCountryDateFormat("dd-MM-yyyy"); OrderProcessingRouteBuilder routeBuilder = new OrderProcessingRouteBuilder(); routeBuilder.inputUri = "direct:in"; routeBuilder.outputUri = "mock:out"; routeBuilder.setOrderFileNameProcessor(processor);
See Chapter 9, Testing, for more details on testing.
See also
- File Component: https://camel.apache.org/file2.html