What about FieldSet!
This is a great new abstraction in Spring Batch, "represents" an anonymous POJO, or better to say a map!
It can be configured declaratively in the config via FixedLengthTokenizer, and later can be used in the ItemWriter, like here:
Code:
<bean id="giroLineTokenizer"
class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="G2,G3,origLine" />
<property name="columns" value="3-5,6-7,1-355" />
<property name="strict" value="false"/>
</bean>
<bean id="fieldSetWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter">
<property name="assertUpdates" value="true" />
<property name="itemSqlParameterSourceProvider" ref="fsSqlParamSourceProvider"/>
<property name="sql"
value="INSERT INTO STAGING_GIRO (item_id, jobId, G2, G3, ORIG_LINE)
VALUES (TEST_SEQ.nextval, :jobId, :G2, :G3, :origLine)" />
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="fsSqlParamSourceProvider" class="example.FieldSetSqlParameterSourceProvider"/>
Certainly some issues have arised:
- ItemReader implementation should return FieldSet instead of business item.
- In case of JdbcBatchItemWriter, a custom ItemSqlParameterSourceProvider is needed.
I've chosen to write a custom LineMapper instead of a full ItemReader, which -in my case- has to support different line types of the input file, so I've borrow from PatternMatchingCompositeLineMapper:
FieldSetLineMapper.java
Code:
public class FieldSetLineMapper implements LineMapper<FieldSet>, InitializingBean {
private PatternMatchingCompositeLineTokenizer tokenizer = new PatternMatchingCompositeLineTokenizer();
@Override
public FieldSet mapLine(String line, int lineNumber) throws Exception {
// delegate
return tokenizer.tokenize(line);
}
@Override
public void afterPropertiesSet() throws Exception {
this.tokenizer.afterPropertiesSet();
}
public void setTokenizers(Map<String, LineTokenizer> tokenizers) {
this.tokenizer.setTokenizers(tokenizers);
}
}
in the config:
Code:
...
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="01001101.020" />
<property name="lineMapper" ref="fsLineMapper"/>
</bean>
<bean id="fsLineMapper" class="example.FieldSetLineMapper">
<property name="tokenizers">
<map>
<entry key="01*" value-ref="kotegFejTokenizer" />
<entry key="03*" value-ref="giroLineTokenizer" />
<entry key="05*" value-ref="kotegLabTokenizer" />
</map>
</property>
</bean>
...
And the final piece is a custom sqlParam provider:
Code:
public class FieldSetSqlParameterSourceProvider implements
ItemSqlParameterSourceProvider<FieldSet>, StepExecutionListener {
private StepExecution stepExecution;
@SuppressWarnings("unchecked")
@Override
public SqlParameterSource createSqlParameterSource(FieldSet item) {
MapSqlParameterSource source = new MapSqlParameterSource();
Properties props = item.getProperties();
Set<String> keys = new HashSet(props.keySet());
for (String key : keys) {
source.addValue(key, props.get(key));
}
// add extra field, e.g. 'jobId' from execution context
addExtraSqlParameters(source);
return source;
}
protected void addExtraSqlParameters(MapSqlParameterSource source) {
source.addValue("jobId", stepExecution.getJobExecution().getJobId());
}
@Override
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
...
}
Actually there is an issue with this setup; every line in the input reaches the writer, (header/footer as well) but the writer can only handle one specific FieldSet type, (the body of the file), so in the processor a filtering must be done!
On the other hand, this works pretty well for me