...
- Customer 1: XML over FTP
- Customer 2: CSV over HTTP
- Customer 3: Excel via e-mail
Now on the Acme side, all this has to be converted to a canonical XML format and submitted to the Acme accounting system via JMS. Then the Acme accounting system does its stuff and sends an XML reply via JMS, with a summary of what it processed (e.g. 3 line items accepted, line item #2 in error, total invoice $123.45). Finally, that data needs to be formatted into an e-mail, and sent to a contact at the customer in question ("Dear Joyce, we received an invoice on 1/2/08. We accepted 3 line items totaling $123.45, though there was an error with line items #2 \ [invalid quantity ordered\]. Thank you for your business. Love, Acme."). Wiki Markup
So it turns out Camel can handle all this:
...
- Add dependencies on Camel-Spring, and the Spring test JAR (which will automatically bring in JUnit 3.8.x) to your POM:
Code Block xml xml <dependency> <artifactId>camel-spring</artifactId> <groupId>org.apache.camel</groupId> <version>1.4.0</version> </dependency> <dependency> <artifactId>spring-test</artifactId> <groupId>org.springframework</groupId> <version>2.5.5</version> <scope>test</scope> </dependency>
- Create a new unit test class in
src/test/java/your-package-here
, perhaps calledXMLInputTest.java
- Make the test extend Spring's AbstractJUnit38SpringContextTests class, so it can load a Spring context for the test
- Create a Spring context configuration file in
src/test/resources
, perhaps calledXMLInputTest-context.xml
- In the unit test class, use the class-level @ContextConfiguration annotation to indicate that a Spring context should be loaded
- By default, this looks for a Context configuration file called
TestClassName-context.xml
in a subdirectory corresponding to the package of the test class. For instance, if your test class wasorg.apache.camel.tutorial.XMLInputTest
, it would look fororg/apache/camel/tutorial/XMLInputTest-context.xml
- To override this default, use the locations attribute on the @ContextConfiguration annotation to provide specific context file locations (starting each path with a / if you don't want it to be relative to the package directory). My solution does this so I can put the context file directly in
src/test/resources
instead of in a package directory under there.
- By default, this looks for a Context configuration file called
- Add a CamelContext instance variable to the test class, with the @Autowired annotation. That way Spring will automatically pull the CamelContext out of the Spring context and inject it into our test class.
- Add a ProducerTemplate instance variable and a
setUp
method that instantiates it from the CamelContext. We'll use the ProducerTemplate later to send messages to the route.Code Block java java protected ProducerTemplate<Exchange> template; protected void setUp() throws Exception { super.setUp(); template = camelContext.createProducerTemplate(); }
- Put in an empty test method just for the moment (so when we run this we can see that "1 test succeeded")
- Add the Spring <beans> element (including the Camel Namespace) with an empty <camelContext> element to the Spring context, like this:
Code Block xml xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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-1.4.0.xsd"> <camelContext id="camel" xmlns="http://activemq.apache.org/camel/schema/spring"> </camelContext> </beans>
...
- Save the input-customer1.xml file to
src/test/resources
- Save your XSLT file (created in the previous step) to
src/main/resources
- Write a Camel Route, either right in the Spring XML, or using the Java DSL (in another class under
src/test/java
somewhere). This route should use the Pipes and Filters integration pattern to:- Start from the endpoint direct:start (which lets the test conveniently pass messages into the route)
- Call the endpoint xslt:YourXSLTFile.xsl (to transform the message with the specified XSLT template)
- Send the result to the endpoint mock:finish (which lets the test verify the route output)
- Add a test method to the unit test class that:
- Get a reference to the Mock endpoint
mock:finish
using code like this:Code Block java java MockEndpoint finish = MockEndpoint.resolve(camelContext, "mock:finish");
- Set the expectedMessageCount on that endpoint to 1
- Get a reference to the Customer 1 input file, using code like this:
Code Block java java InputStream in = XMLInputTest.class.getResourceAsStream("/input-partner1.xml"); assertNotNull(in);
- Send that InputStream as a message to the
direct:start
endpoint, using code like this:
Note that we can send the sample file body in several formats (File, InputStream, String, etc.) but in this case an InputStream is pretty convenient.Code Block java java template.sendBody("direct:start", in);
- Ensure that the message made it through the route to the final endpoint, by testing all configured Mock endpoints like this:
Code Block java java MockEndpoint.assertIsSatisfied(camelContext);
- If you like, inspect the final message body using some code like
finish.getExchanges().get(0).getIn().getBody()
.- If you do this, you'll need to know what format that body is – String, byte array, InputStream, etc.
- Get a reference to the Mock endpoint
- Run your test with mvn install and make sure the build completes successfully.
...
- Create a new class under
src/main/java
, perhaps calledCSVConverterBean
. - Add a method, with one argument of type
List<List<String>>
and the return typeInvoice
- In the method, the logic should look roughly like this:
- Create a new
Invoice
, using the method on the generatedObjectFactory
class - Loop through all the rows in the incoming CSV (the outer
List
) - Skip the first row, which contains headers (column names)
- For the other rows:
- Create a new
LineItemType
(using theObjectFactory
again) - Pick out all the cell values (the Strings in the inner
List
) and put them into the correct fields of theLineItemType
- Not all of the values will actually go into the line item in this example
- You may hardcode the column ordering based on the sample data file, or else try to read it dynamically from the headers in the first line
- Note that you'll need to use a JAXB
DatatypeFactory
to create theXMLGregorianCalendar
values that JAXB uses for thedate
fields in the XML – which probably means using aSimpleDateFormat
to parse the date and setting that date on aGregorianCalendar
- Add the line item to the invoice
- Create a new
- Populate the partner ID, date of receipt, and order total on the
Invoice
- Throw any exceptions out of the method, so Camel knows something went wrong
- Return the finished
Invoice
- Create a new
...
Camel does not have a data format handler for Excel by default. We have two options – create an Excel DataFormat (so Camel can convert Excel spreadsheets to something like the CSV List<List<String>>
automatically), or create a POJO that can translate Excel data manually. For now, the second approach is easier (if we go the DataFormat
route, we need code to both read and write Excel files, whereas otherwise read-only will do).
...
- Update the Maven POM to include POI support:
Code Block xml xml <dependency> <artifactId>poi</artifactId> <groupId>org.apache.poi</groupId> <version>3.1-FINAL</version> </dependency>
- Create a new class under
src/main/java
, perhaps calledExcelConverterBean
. - Add a method, with one argument of type
InputStream
and the return typeInvoice
- In the method, the logic should look roughly like this:
- Create a new
Invoice
, using the method on the generatedObjectFactory
class - Create a new HSSFWorkbook from the
InputStream
, and get the first sheet from it - Loop through all the rows in the sheet
- Skip the first row, which contains headers (column names)
- For the other rows:
- Create a new
LineItemType
(using theObjectFactory
again) - Pick out all the cell values and put them into the correct fields of the
LineItemType
(you'll need some data type conversion logic)- Not all of the values will actually go into the line item in this example
- You may hardcode the column ordering based on the sample data file, or else try to read it dynamically from the headers in the first line
- Note that you'll need to use a JAXB
DatatypeFactory
to create theXMLGregorianCalendar
values that JAXB uses for thedate
fields in the XML – which probably means setting the date from a date cell on aGregorianCalendar
- Add the line item to the invoice
- Create a new
- Populate the partner ID, date of receipt, and order total on the
Invoice
- Throw any exceptions out of the method, so Camel knows something went wrong
- Return the finished
Invoice
- Create a new
...