Tuesday, March 15, 2011

Spring Batch -- Unplugged

Recently I was tasked with support a legacy warehouse management system (WMS) that replied on fixed width files to process online customer orders. The current system uses IBM MQ and process event triggers. Basically, you drop a fixed width message on a queue, MQ triggers a process to run a scripts on the file system, an executable gets the message off the queue and writes it to the file system.

The application that generates the message is IBM Websphere Message Broker. Not wanting to continuing using Message Broker, I proposed using Spring as an alternative. But choosing Spring meant re-doing the fixed width message mapping currently done in message broker. Then I remembered something I heard during a recent Spring Source training workshop my employer had sponsored: there was fixed width file support in Spring Batch.

Looking at Spring Batch, I found what I was looking for, notably the FlatFileItemWriter. Liking what I saw, I tried to wrap my head around what Spring Batch had to offer and how to use it. After immersing myself in online documentation, I came to the conclusion that the way Spring Batch worked did not fit the intended implementation I had in mind. My application was to be a fairly generic Message Driven POJO using basic XML Spring configuration. Whereas Spring Batch is for, well, batch jobs. Clearly not what I was looking for.

So I figured why not just use the classes I needed from Spring Batch and inject them into my application. I added the following to my application context to use the FlatFileItemWriter in my app:



<bean id="itemWriter015" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" value="file:rdm.txt" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="actionType,crmOrderNumber" />
</bean>
</property>
<property name="format" value="015%1s%9s" />
</bean>
</property>
</bean>


As you can see, there is also a mapper involve that uses the OXM marshallers from Spring to map an order XML to domain model objects. The domain model objects have standard getters and setters. The getters are utilized by the FormatterLineAggregator; another Spring Batch object.

FormatterLineAggregator

The FormatterLineAggregator has something called a field extractor which is an instance of the Spring Batch BeanWrapperFieldExtractor class. It is on the BeanWrapperFieldExtractor where you provide the list of properties you want to write out using the line aggregator. The values in this list must match the getter methods of the object you pass to the item writer's 'write' method. As in:


List sohList = new ArrayList();
sohList.add(stockOrder.getHeader());

try {
getFfiw015().open(new ExecutionContext());
getFfiw015().write(sohList);
getFfiw015().close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}


Here I am passing something called a stock order object that has get methods for the actionType and crmOrderNumber members. These values were set by reading the XML and unmarshalling into the stock order domain model objects.

Spring OXM Unmarshalling

To configure XML unmarshalling to Java, here is the bean definitions from the application context:


<bean id="sodMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<list>
<value>com.gap.gid.stockorder.model.StockOrder</value>
</list>
</property>
<property name="schemas">
<list>
<value>classpath:com/gap/gid/stockorder/model/schema/omssod.xsd</value>
</list>
</property>
</bean>

<bean id="stockOrderMapper" class="com.gap.gid.stockorder.model.StockOrderMapper">
<property name="marshaller">
<ref bean="sodMarshaller" />
</property>
<property name="unmarshaller">
<ref bean="sodMarshaller" />
</property>
</bean>


After grabbing the XML message off the queue, hand it off to the mapper/unmarshaller:


InputStream is = null;
StockOrder stockOrder = null;
try {
is = new ByteArrayInputStream(xmlMessage.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
stockOrder = (StockOrder) stockOrderMapper.readObjectFromXml(new StreamSource(is));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}


The unmarshaller knows which elements to map to which object members by using annotations:


@XmlRootElement(name="StockOrder")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
public class StockOrder implements Serializable {

/**
*
*/
private static final long serialVersionUID = 9126450659071953120L;
/**
*
*/

@XmlElement(name="StockOrderHeader")
private StockOrderHeader header;

/**
* @return the header
*/
public StockOrderHeader getHeader() {
return header;
}

/**
* @param header the header to set
*/
public void setHeader(StockOrderHeader header) {
this.header = header;
}

}