This recipe will show you a strategy for normalizing input that arrives in many different data formats (CSV, JSON, and XML) into a single canonical XML format. The strategy shown here also allows you to handle unknown input formats.
This strategy uses the Normalizer EIP, which is a Content Based Router EIP (see the Content Based Routing recipe in Chapter 2, Message Routing) combined with Data Format translators. It detects the incoming message format, and performs one or more steps needed to transform the message to the canonical format. After the Content Based Router, all processing steps can now operate assuming the message is in canonical format.
The Java code for this recipe is located in the org.camelcookbook.transformation.normalizer
package. Spring XML files are located under src/main/resources/META-INF/spring
and prefixed with normalizer
.
The main idea behind normalizing messages is to use a Content Based Router (see the Content Based Routing recipe in Chapter 2, Message Routing) whose when
statements identify and transform each of the different formats to be normalized:
In the XML DSL, this is written as:
<route> <from uri="direct:start"/> <choice> <!-- normalize each input format here --> </choice> <to uri="mock:normalized"/> </route>
In the Java DSL, the same thing is expressed as:
public void configure() throws Exception { // setup Data Formats here from("direct:start") .choice() // normalize each input format here .end() .to("mock:normalized"); }
The mock:normalized
endpoint should only receive messages as a graph of JAXB objects regardless of the format of the input message to this route.
In the XML DSL, this routing logic is written as:
<choice> <when> <simple> ${header.CamelFileName} regex '.*.csv$' </simple> <unmarshal> <bindy type="Csv" packages="org.camelcookbook.transformation.csv.model"/> </unmarshal> <bean ref="myNormalizer" method="bookModelToJaxb"/> <to uri="mock:csv"/> </when> <!-- normalize other formats --> </choice>
In that Java DSL, the same thing is written as follows:
.choice() .when(header(Exchange.FILE_NAME).endsWith(".csv")) .unmarshal(bindy) .bean(MyNormalizer.class, "bookModelToJaxb") .to("mock:csv") // normalize other formats .end()
The MyNormalizer
Java call-out is to code that translates from the CSV Java model to JAXB.
Note that the XML DSL uses the Simple Expression Language's regular expression operator (regex
) to detect the filename extension, while the Java DSL uses the Header Expression with a value builder predicate. It felt more natural in the Java DSL to use a Java-like expression (endsWith
) versus a regular expression. There is no "best" approach in situations like this; use whichever you feel best matches your needs.
In the XML DSL, this is written as:
<choice> <when> <simple> ${header.CamelFileName} regex '.*.json$' </simple> <unmarshal> <xmljson rootName="bookstore" elementName="book" expandableProperties="author author"/> </unmarshal> <to uri="mock:json"/> </when> <!-- normalize other formats --> </choice>
In that Java DSL, this is written as follows:
.choice() .when(header(Exchange.FILE_NAME).endsWith(".json")) .unmarshal(xmlJsonFormat) .to("mock:json") // normalize other formats .end()
In the XML DSL, this logic is written as:
<choice> <when> <simple> ${header.CamelFileName} regex '.*.xml$' </simple> <unmarshal> <jaxb contextPath="org.camelcookbook.transformation.myschema"/> </unmarshal> <to uri="mock:xml"/> </when> <!-- normalize other formats --> </choice>
In that Java DSL, this is written as follows:
.choice() .when(header(Exchange.FILE_NAME).endsWith(".xml")) .unmarshal(jaxb) .to("mock:xml") // normalize other formats .end()
stop
statement to discontinue processing of this message within this route.In the XML DSL, this step is written as:
<choice> <!-- normalize input formats --> <otherwise> <to uri="mock:unknown"/> <stop/> </otherwise> </choice>
In that Java DSL, the same thing is expressed as:
.choice() // normalize input formats .otherwise() .to("mock:unknown") .stop() .end()
The main part of this recipe uses the Content Based Routing EIP to detect each incoming input message format, and performs the steps required to normalize that message to the desired canonical format.
A number of different types of expressions can be used to detect the incoming message format. The preceding example uses the CamelFileName
header that is set by the File and FTP Components, to detect the input format based on the suffix of the filename. For a scenario with different XML inputs, using an XPath Expression and inspecting Namespaces might be a better way to normalize different versions of XML requests, followed by translations using XSLT or XQuery into the common XML format.
The otherwise
portion of the Content Based Router is used in the example to catch other unknown input message formats that are not handled by the when
statement's expressions. It is common in such cases to log or send the message to some external route or processor for later handling by a person or automated error-handling portion of your application. The route could also throw an exception, such as an IllegalArgumentException
, if you want the caller to handle the issue. The Camel stop
statement in this example tells Camel to stop processing, that is, to execute no more processing steps with this message within this route.
In this example, within the processor steps for normalizing incoming CSV format messages, we see the usage of a Java callout to translate one Java object model to another. If this Java model conversation is common across your Camel routes, you could create and register a Camel Data Type Converter to allow Camel to automatically do the type conversation for you when needed. In this example, the Java callout to MyNormalizer
could be replaced with the use of the Camel directive convertBodyTo(org.camelcookbook.transformation.myschema.Bookstore)
, which arguably is clearer as to what's happening within the route. See the Writing a custom data type converter recipe in Chapter 3, Routing to Your Code for more details.
18.189.186.167