From df2618d4181eb75333cda438796c4ff25ebb9e32 Mon Sep 17 00:00:00 2001 From: dhgarrette Date: Fri, 5 Jun 2009 03:57:01 +0000 Subject: [PATCH] BATCH-1270: Update documentation for formatting consistency --- src/site/docbook/reference/job.xml | 316 ++--- .../docbook/reference/readersAndWriters.xml | 1200 +++++++---------- src/site/docbook/reference/step.xml | 35 +- 3 files changed, 662 insertions(+), 889 deletions(-) diff --git a/src/site/docbook/reference/job.xml b/src/site/docbook/reference/job.xml index ce93d5770..3a20276df 100644 --- a/src/site/docbook/reference/job.xml +++ b/src/site/docbook/reference/job.xml @@ -10,12 +10,12 @@ + fileref="images/spring-batch-reference-model.png" scale="80" /> @@ -36,27 +36,21 @@ three required dependencies: a name, JobRepository, and a list of Steps. - - <job id="footballJob"> + <job id="footballJob"> <step id="playerload" parent="s1" next="gameLoad"/> <step id="gameLoad" parent="s2" next="playerSummarization"/> <step id="playerSummarization" parent="s3"/> - </job> - - +</job> The namespace defaults to referencing a repository with an id of 'jobRepository', which is a sensible default. However, this can be overridden explicitly: - - <job id="footballJob" job-repository="specialRepository"> + <job id="footballJob" job-repository="specialRepository"> <step id="playerload" parent="s1" next="gameLoad"/> <step id="gameLoad" parent="s3" next="playerSummarization"/> <step id="playerSummarization" parent="s3"/> - </job> - - +</job>
Restartability @@ -74,38 +68,30 @@ be run as part of a new JobInstance, then the restartable property may be set to 'false': - - <job id="footballJob" restartable="false"> - <step id="playerload" parent="s1" next="gameLoad"/> - <step id="gameLoad" parent="s2" next="playerSummarization"/> - <step id="playerSummarization" parent="s3"/> - </job> - - + <job id="footballJob" restartable="false"> + ... +</job> To phrase it another way, setting restartable to false means "this Job does not support being started again". Restarting a Job that is not restartable will cause a JobRestartException to be thrown: - - Job job = new SimpleJob(); - job.setRestartable(false); + Job job = new SimpleJob(); +job.setRestartable(false); - JobParameters jobParameters = new JobParameters(); +JobParameters jobParameters = new JobParameters(); - JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters); - jobRepository.saveOrUpdate(firstExecution); +JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters); +jobRepository.saveOrUpdate(firstExecution); - try { +try { jobRepository.createJobExecution(job, jobParameters); fail(); - } - catch (JobRestartException e) { +} +catch (JobRestartException e) { // expected - } - - +} This snippet of JUnit code shows how attempting to create a JobExecution the first time for a non restartable @@ -122,49 +108,40 @@ SimpleJob allows for this by calling a JobListener at the appropriate time: - - public interface JobExecutionListener { + public interface JobExecutionListener { void beforeJob(JobExecution jobExecution); void afterJob(JobExecution jobExecution); - } - - +} JobListeners can be added to a SimpleJob via the listeners element on the job: - - <job id="footballJob"> + <job id="footballJob"> <step id="playerload" parent="s1" next="gameLoad"/> <step id="gameLoad" parent="s2" next="playerSummarization"/> <step id="playerSummarization" parent="s3"/> - <listeners> - <listener class="org.springframework.batch.sample.SampleListener"/> + <listeners> + <listener class="org.springframework.batch.sample.SampleListener"/> </listeners> - </job> - - +</job> It should be noted that afterJob will be called regardless of the success or failure of the Job. If success or failure needs to be determined it can be obtained from the JobExecution: - - void afterJob(JobExecution jobExecution){ + public void afterJob(JobExecution jobExecution){ if( jobExecution.getStatus() == BatchStatus.COMPLETED ){ - //job success + //job success } else if(jobExecution.getStatus() == BatchStatus.FAILED){ - //job failure + //job failure } - } - - +} The annotations corresponding to this interface are: @@ -197,22 +174,19 @@ Job with two listeners and one Step, "step1". - - <job id="baseJob" abstract="true"> + <job id="baseJob" abstract="true"> <listeners> - <listener class="com.ListenerOne"/> + <listener class="com.ListenerOne"/> <listeners> - </job> +</job> - <job id="job1" parent="baseJob3"> +<job id="job1" parent="baseJob3"> <step id="step1" parent="standaloneStep"/> <listeners merge="true"> - <listener class="com.ListenerTwo"/> + <listener class="com.ListenerTwo"/> <listeners> - </job> - - +</job> Please see the section on Inheriting from a Parent Step @@ -262,15 +236,12 @@ collaborators. However, there are still a few configuration options available: - - <job-repository id="jobRepository" + <job-repository id="jobRepository" dataSource="dataSource" transactionManager="transactionManager" isolation-level-for-create="serializable" table-prefix="BATCH_" - /> - - +/> None of the configuration options listed above are required except the id. If they are not set, the defaults shown above will be used. They @@ -295,30 +266,24 @@ that the SERIALIZED will cause problems, as long as the database platform supports it. However, this can be overridden: - - <job-repository id="jobRepository" - isolation-level-for-create="ISOLATION_REPEATABLE_READ" /> - - + <job-repository id="jobRepository" + isolation-level-for-create="ISOLATION_REPEATABLE_READ" /> If the namespace or factory beans aren't used then it is also essential to configure the transactional behavior of the repository using AOP: - - <aop:config> - <aop:advisor - pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))" - <advice-ref="txAdvice" /> - </aop:config> + <aop:config> + <aop:advisor + pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"/> + <advice-ref="txAdvice" /> +</aop:config> - <tx:advice id="txAdvice" transaction-manager="transactionManager"> - <tx:attributes> - <tx:method name="*" /> - </tx:attributes> - </tx:advice> - - +<tx:advice id="txAdvice" transaction-manager="transactionManager"> + <tx:attributes> + <tx:method name="*" /> + </tx:attributes> +</tx:advice> This fragment can be used as is, with almost no changes. Remember also to include the appropriate namespace declarations and to make sure @@ -338,12 +303,8 @@ meta data tables is needed within the same schema, then the table prefix will need to be changed: - - <job-repository id="jobRepository" - table-prefix="SYSTEM.TEST_" - /> - - + <job-repository id="jobRepository" + table-prefix="SYSTEM.TEST_" /> Given the above changes, every query to the meta data tables will be prefixed with "SYSTEM.TEST_". BATCH_JOB_EXECUTION will be referred to @@ -363,13 +324,12 @@ objects at each commit point takes extra time. Another reason may be that you just don't need to persist status for a particular job. For this reason, Spring batch provides an in-memory Map version of the job - respository: + repository: - - - -]]> + <bean id="jobRepository" + class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"> + <property name="transactionManager" ref="transactionManager"/> +</bean> Note that the in-memory repository is volatile and so does not allow restart between JVM instances. It also cannot guarantee that two @@ -377,14 +337,11 @@ use the database version of the repository wherever you need quality of service. - However it does require a transaction manager to be - defined because there are rollback semantics within the - repository, and because the business logic might still be - transactional (e.g. RDBMS access). For testing purposes many - people find - the ResourcelessTransactionManager - useful. - + However it does require a transaction manager to be defined + because there are rollback semantics within the repository, and because + the business logic might still be transactional (e.g. RDBMS access). For + testing purposes many people find the + ResourcelessTransactionManager useful.
@@ -397,12 +354,10 @@ shortcut and use it to set the database type to the closest match: - -<bean id="jobRepository" class="org...JobRepositoryFactoryBean"> - <property name="databaseType" value="db2"/> - <property name="dataSource" ref="dataSource"/> -</bean> - + <bean id="jobRepository" class="org...JobRepositoryFactoryBean"> + <property name="databaseType" value="db2"/> + <property name="dataSource" ref="dataSource"/> +</bean> (The JobRepositoryFactoryBean tries to auto-detect the database type from the DataSource @@ -428,10 +383,10 @@ a JobRepository, in order to obtain an execution: - <bean id="jobLauncher" - class="org.springframework.batch.execution.launch.SimpleJobLauncher"> + <bean id="jobLauncher" + class="org.springframework.batch.execution.launch.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> - </bean> +</bean> Once a JobExecution is @@ -479,13 +434,13 @@ configured to allow for this scenario by configuring a TaskExecutor: - <bean id="jobLauncher" - class="org.springframework.batch.execution.launch.SimpleJobLauncher"> + <bean id="jobLauncher" + class="org.springframework.batch.execution.launch.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> <property name="taskExecutor"> - <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" /> + <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" /> </property> - </bean> +</bean> Any implementation of the spring TaskExecutor interface can be used to control how jobs are asynchronously @@ -594,16 +549,13 @@ will be converted into JobParameters. An example of the XML configuration is below: - <job id="endOfDay"> - <steps> - <step id="step1" parent="simpleStep" /> - <!-- Step details removed for clarity --> - </steps> - </job> + <job id="endOfDay"> + <step id="step1" parent="simpleStep" /> +</job> - <!-- Launcher details removed for clarity --> - <beans: bean id="jobLauncher" - class="org.springframework.batch.core.launch.support.SimpleJobLauncher" /> +<!-- Launcher details removed for clarity --> +<beans:bean id="jobLauncher" + class="org.springframework.batch.core.launch.support.SimpleJobLauncher" /> This example is overly simplistic, since there are many more requirements to a run a batch job in Spring Batch in general, but it @@ -641,9 +593,10 @@ to a number using the ExitCodeMapper interface: - public interface ExitCodeMapper { + public interface ExitCodeMapper { public int intValue(String exitCode); + } The essential contract of an @@ -703,9 +656,8 @@ is required when handling an HttpRequest. An example is below: - - @Controller - public class JobLauncherController { + @Controller +public class JobLauncherController { @Autowired JobLauncher jobLauncher; @@ -715,11 +667,9 @@ @RequestMapping("/jobLauncher.html") public void handle() throws Exception{ - jobLauncher.run(job, new JobParameters()); + jobLauncher.run(job, new JobParameters()); } - } - - +}
@@ -776,8 +726,7 @@ query the repository for existing executions. This functionality is provided by the JobExplorer interface: - - public interface JobExplorer { + public interface JobExplorer { List<JobInstance> getJobInstances(String jobName, int start, int count); @@ -790,9 +739,7 @@ List<JobExecution> getJobExecutions(JobInstance jobInstance); Set<JobExecution> findRunningJobExecutions(String jobName); - } - - +} As is evident from the method signatures above, JobExplorer is a read-only version of the @@ -800,11 +747,8 @@ JobRepository, it can be easily configured via a factory bean: - - <bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean" - p:dataSource-ref="dataSource" /> - - + <bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean" + p:dataSource-ref="dataSource" /> Earlier in this chapter, it was mentioned that the table prefix of the @@ -813,11 +757,8 @@ JobExplorer is working with the same tables, it too needs the ability to set a prefix: - - <bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean" - p:dataSource-ref="dataSource" p:tablePrefix="BATCH_" /> - - + <bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean" + p:dataSource-ref="dataSource" p:tablePrefix="BATCH_" />
@@ -832,39 +773,39 @@ provides for these types of operations via the JobOperator interface: - - public interface JobOperator { + public interface JobOperator { List<Long> getExecutions(long instanceId) throws NoSuchJobInstanceException; - List<Long> getJobInstances(String jobName, int start, int count) throws NoSuchJobException; + List<Long> getJobInstances(String jobName, int start, int count) + throws NoSuchJobException; Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException; String getParameters(long executionId) throws NoSuchJobExecutionException; Long start(String jobName, String parameters) - throws NoSuchJobException, JobInstanceAlreadyExistsException; + throws NoSuchJobException, JobInstanceAlreadyExistsException; Long restart(long executionId) - throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, + throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, NoSuchJobException, JobRestartException; Long startNextInstance(String jobName) - throws NoSuchJobException, JobParametersNotFoundException, JobRestartException, - JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException; + throws NoSuchJobException, JobParametersNotFoundException, JobRestartException, + JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException; - boolean stop(long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException; + boolean stop(long executionId) + throws NoSuchJobExecutionException, JobExecutionNotRunningException; String getSummary(long executionId) throws NoSuchJobExecutionException; - Map<Long, String> getStepExecutionSummaries(long executionId) throws NoSuchJobExecutionException; + Map<Long, String> getStepExecutionSummaries(long executionId) + throws NoSuchJobExecutionException; Set<String> getJobNames(); - } - - +} The above operations represent methods from many different interfaces, such as JobLauncher, @@ -874,19 +815,16 @@ implementation of JobOperator, SimpleJobOperator, has many dependencies: - - <bean id="jobOperator" class="org.springframework.batch.core.launch.support.SimpleJobOperator"> + <bean id="jobOperator" class="org.spr...SimpleJobOperator"> <property name="jobExplorer"> - <bean class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"> - <property name="dataSource" ref="dataSource" /> - </bean> + <bean class="org.spr...JobExplorerFactoryBean"> + <property name="dataSource" ref="dataSource" /> + </bean> </property> <property name="jobRepository" ref="jobRepository" /> <property name="jobRegistry" ref="jobRegistry" /> <property name="jobLauncher" ref="jobLauncher" /> - </bean> - - +</bean>
@@ -911,14 +849,11 @@ Job to force the Job to a new instance: - - public interface JobParametersIncrementer { + public interface JobParametersIncrementer { JobParameters getNext(JobParameters parameters); - } - - +} The contract of JobParametersIncrementer is that, given a Job, as shown below: - - public class SampleIncrementer implements JobParametersIncrementer { + public class SampleIncrementer implements JobParametersIncrementer { public JobParameters getNext(JobParameters parameters) { - if (parameters==null || parameters.isEmpty()) { - return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters(); - } - long id = parameters.getLong("run.id",1L) + 1; - return new JobParametersBuilder().addLong("run.id", id).toJobParameters(); + if (parameters==null || parameters.isEmpty()) { + return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters(); + } + long id = parameters.getLong("run.id",1L) + 1; + return new JobParametersBuilder().addLong("run.id", id).toJobParameters(); } -} - - +} In this example, the value with a key of 'run.id' is used to discriminate between JobInstances. If the @@ -957,14 +889,9 @@ be associated with Job via the 'incrementer' attribute in the namespace: - - <job id="footballJob" incrementer="sampleIncrementer"> - <step id="playerload" parent="s1" next="gameLoad"/> - <step id="gameLoad" parent="s2" next="playerSummarization"/> - <step id="playerSummarization" parent="s3"/> - </job> - - + <job id="footballJob" incrementer="sampleIncrementer"> + ... +</job>
@@ -974,11 +901,8 @@ JobOperator is gracefully stopping a Job: - - Set<Long> executions = jobOperator.getRunningExecutions("sampleJob"); - - jobOperator.stop(executions.iterator().next()); - + Set<Long> executions = jobOperator.getRunningExecutions("sampleJob"); +jobOperator.stop(executions.iterator().next()); The shutdown is not immediate, since there is no way to force immediate shutdown, especially if the execution is currently in diff --git a/src/site/docbook/reference/readersAndWriters.xml b/src/site/docbook/reference/readersAndWriters.xml index 101026dd1..b87ee0fea 100644 --- a/src/site/docbook/reference/readersAndWriters.xml +++ b/src/site/docbook/reference/readersAndWriters.xml @@ -48,10 +48,9 @@ public interface ItemReader<T> { - T read() throws Exception, UnexpectedInputException, ParseException; + T read() throws Exception, UnexpectedInputException, ParseException; -} - +} The read method defines the most essential contract of the ItemReader; calling it returns one @@ -87,10 +86,9 @@ public interface ItemWriter<T> { - void write(List<? extends T> items) throws Exception; + void write(List<? extends T> items) throws Exception; -} - +} As with read on ItemReader, write provides @@ -117,23 +115,21 @@ that contains another ItemReader. For example: - public class CompositeItemWriter<T> implements ItemWriter<T> { + public class CompositeItemWriter<T> implements ItemWriter<T> { ItemWriter<T> itemWriter; public CompositeItemWriter(ItemWriter<T> itemWriter) { - this.itemWriter = itemWriter; + this.itemWriter = itemWriter; } public void write(List<? extends T> items) throws Exception { - - //Add business logic here - - itemWriter.write(item); + //Add business logic here + itemWriter.write(item); } public void setDelegate(ItemWriter<T> itemWriter){ - this.itemWriter = itemWriter; + this.itemWriter = itemWriter; } } @@ -149,9 +145,9 @@ For this scenario, Spring Batch provides the ItemProcessor interface: - public interface ItemProcessor<I, O> { + public interface ItemProcessor<I, O> { - O process(I item) throws Exception; + O process(I item) throws Exception; } An ItemProcessor is very simple; given one @@ -164,28 +160,24 @@ written out. An ItemProcessor can be written that performs the conversion: - public class Foo {} + public class Foo {} - public class Bar { +public class Bar { public Bar(Foo foo) {} - } +} - public class FooProcessor implements ItemProcessor<Foo,Bar>{ - - //Perform simple transformation, convert a Foo to a Bar +public class FooProcessor implements ItemProcessor<Foo,Bar>{ public Bar process(Foo foo) throws Exception { - return new Bar(foo); + //Perform simple transformation, convert a Foo to a Bar + return new Bar(foo); } - } - - public class BarWriter implements ItemWriter<Bar>{ +} +public class BarWriter implements ItemWriter<Bar>{ public void write(List<? extends Bar> bars) throws Exception { - //write bars + //write bars } - - //rest of class ommitted for clarity - } +} In the very simple example above, there is a class Foo, a class Bar, and a @@ -199,16 +191,14 @@ provided. The FooProcessor can then be injected into a Step: - - <job id="ioSampleJob"> + <job id="ioSampleJob"> <step name="step1"> - <tasklet> - <chunk reader="fooReader" processor="fooProcessor" writer="barWriter" commit-interval="2"/> - </tasklet> + <tasklet> + <chunk reader="fooReader" processor="fooProcessor" writer="barWriter" + commit-interval="2"/> + </tasklet> </step> - </job> - - +</job>
Chaining ItemProcessors @@ -221,73 +211,67 @@ transformed to Bar, which will be transformed to Foobar and written out: - public class Foo {} + public class Foo {} - public class Bar { +public class Bar { public Bar(Foo foo) {} - } +} - public class Foobar{ - public Foobar(Bar bar){} - } +public class Foobar{ + public Foobar(Bar bar) {} +} - public class FooProcessor implements ItemProcessor<Foo,Bar>{ - - //Perform simple transformation, convert a Foo to a Bar +public class FooProcessor implements ItemProcessor<Foo,Bar>{ public Bar process(Foo foo) throws Exception { - return new Bar(foo); + //Perform simple transformation, convert a Foo to a Bar + return new Bar(foo); } - } - - public class BarProcessor implements ItemProcessor<Bar,FooBar>{ +} +public class BarProcessor implements ItemProcessor<Bar,FooBar>{ public FooBar process(Bar bar) throws Exception { - return new Foobar(bar); + return new Foobar(bar); } - } - - public class FoobarWriter implements ItemWriter<FooBar>{ +} +public class FoobarWriter implements ItemWriter<FooBar>{ public void write(List<? extends FooBar> items) throws Exception { - //write items + //write items } - - //rest of class ommitted for clarity - } +} - A FooTransformer and - BarTransformer can be 'chained' together to give + A FooProcessor and + BarProcessor can be 'chained' together to give the resultant Foobar: - CompositeItemProcessor<Foo,Foobar> compositeProcessor = new CompositeItemProcessor<Foo,Foobar>(); - List itemProcessors = new ArrayList(); - itemProcessors.add(new FooTransformer()); - itemProcessors.add(new BarTransformer()); - compositeProcessor.setItemProcessors(itemProcessors); + CompositeItemProcessor<Foo,Foobar> compositeProcessor = + new CompositeItemProcessor<Foo,Foobar>(); +List itemProcessors = new ArrayList(); +itemProcessors.add(new FooTransformer()); +itemProcessors.add(new BarTransformer()); +compositeProcessor.setItemProcessors(itemProcessors); Just as with the previous example, the composite processor can be configured into the Step: - - <job id="ioSampleJob"> + <job id="ioSampleJob"> <step name="step1"> - <tasklet> - <chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter" commit-interval="2"/> - </tasklet> + <tasklet> + <chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter" + commit-interval="2"/> + </tasklet> </step> - </job> +</job> - <bean id="compositeItemProcessor" - class="org.springframework.batch.item.support.CompositeItemProcessor"> +<bean id="compositeItemProcessor" + class="org.springframework.batch.item.support.CompositeItemProcessor"> <property name="itemProcessors"> - <list> - <bean class="..FooProcessor" /> - <bean class="..BarProcessor" /> - </list> + <list> + <bean class="..FooProcessor" /> + <bean class="..BarProcessor" /> + </list> </property> - </bean> - - +</bean>
@@ -295,7 +279,7 @@ One typical use for an item processor is to filter out records before they are passed to the ItemWriter. Filtering is an action - distinct from skpping; skipping indicates that a record is invalid + distinct from skipping; skipping indicates that a record is invalid whereas filtering simply indicates that a record should not be written. @@ -329,13 +313,12 @@ public interface ItemStream { - void open(ExecutionContext executionContext) throws ItemStreamException; + void open(ExecutionContext executionContext) throws ItemStreamException; - void update(ExecutionContext executionContext) throws ItemStreamException; + void update(ExecutionContext executionContext) throws ItemStreamException; - void close() throws ItemStreamException; -} - + void close() throws ItemStreamException; +} Before describing each method, we should mention the ExecutionContext. Clients of an @@ -384,27 +367,24 @@ are not known to the Step, they need to be injected as listeners or streams (or both if appropriate): - - <job id="ioSampleJob"> + <job id="ioSampleJob"> <step name="step1"> - <tasklet> - <chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter" - commit-interval="2"> - <streams> - <stream ref="barWriter" /> - </streams> - </chunk> - </tasklet> + <tasklet> + <chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter" + commit-interval="2"> + <streams> + <stream ref="barWriter" /> + </streams> + </chunk> + </tasklet> </step> - </job> +</job> - <bean id="compositeItemWriter" class="...CompositeItemWriter"> +<bean id="compositeItemWriter" class="...CompositeItemWriter"> <property name="delegate" ref="barWriter" /> - </bean> +</bean> - <bean id="barWriter" class="...BarWriter" /> - - +<bean id="barWriter" class="...BarWriter" />
@@ -436,14 +416,11 @@ fields so that the fields may be accessed either by index or name as patterned after ResultSet: - - String[] tokens = new String[]{"foo", "1", "true"}; - FieldSet fs = new DefaultFieldSet(tokens); - String name = fs.readString(0); - int value = fs.readInt(1); - boolean booleanValue = fs.readBoolean(2); - - + String[] tokens = new String[]{"foo", "1", "true"}; +FieldSet fs = new DefaultFieldSet(tokens); +String name = fs.readString(0); +int value = fs.readInt(1); +boolean booleanValue = fs.readBoolean(2); There are many more options on the FieldSet interface, such as Date, long, @@ -473,9 +450,7 @@ Framework, Chapter 4.Resources. Therefore, this guide will not go into the details of creating Resource objects. However, a simple example of a - file system resource can be found below: - Resource resource = new FileSystemResource("resources/trades.csv"); - + file system resource can be found below: Resource resource = new FileSystemResource("resources/trades.csv"); In complex batch environments the directory structures are often managed by the EAI infrastructure where drop zones for external @@ -594,13 +569,11 @@ level construct such as ResultSet and returns an Object, flat file processing requires the same construct to convert a String line into an - Object: - public interface LineMapper<T> { + Object:public interface LineMapper<T> { T mapLine(String line, int lineNumber) throws Exception; - } - +} The basic contract is that, given the current line and the line number with which it is associated, the mapper should return a @@ -626,14 +599,11 @@ FieldSet. In Spring Batch, this interface is the LineTokenizer: - - public interface LineTokenizer { + public interface LineTokenizer { FieldSet tokenize(String line); - } - - +} The contract of a LineTokenizer is such that, given a line of input (in theory the @@ -678,14 +648,11 @@ LineTokenizer to translate a line of data from a resource into an object of the desired type: - - public interface FieldSetMapper<T> { + public interface FieldSetMapper<T> { T mapFieldSet(FieldSet fieldSet); - } - - +} The pattern used is the same as the RowMapper used by @@ -728,27 +695,24 @@ DefaultLineMapper represents the behavior most users will need: - - public class DefaultLineMapper<T> implements LineMapper<T>, InitializingBean { + public class DefaultLineMapper<T> implements LineMapper<T>, InitializingBean { private LineTokenizer tokenizer; private FieldSetMapper<T> fieldSetMapper; public T mapLine(String line, int lineNumber) throws Exception { - return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line)); + return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line)); } public void setLineTokenizer(LineTokenizer tokenizer) { - this.tokenizer = tokenizer; + this.tokenizer = tokenizer; } public void setFieldSetMapper(FieldSetMapper<T> fieldSetMapper) { - this.fieldSetMapper = fieldSetMapper; + this.fieldSetMapper = fieldSetMapper; } - } - - +} The above functionality is provided in a default implementation, rather than being built into the reader itself (as was done in @@ -762,35 +726,33 @@ The following example will be used to illustrate this using an actual domain scenario. This particular batch job reads in football - players from the following file: ID,lastName,firstName,position,birthYear,debutYear - "AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996", - "AbduRa00,Abdullah,Rabih,rb,1975,1999", - "AberWa00,Abercrombie,Walter,rb,1959,1982", - "AbraDa00,Abramowicz,Danny,wr,1945,1967", - "AdamBo00,Adams,Bob,te,1946,1969", - "AdamCh00,Adams,Charlie,wr,1979,2003" + players from the following file:ID,lastName,firstName,position,birthYear,debutYear +"AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996", +"AbduRa00,Abdullah,Rabih,rb,1975,1999", +"AberWa00,Abercrombie,Walter,rb,1959,1982", +"AbraDa00,Abramowicz,Danny,wr,1945,1967", +"AdamBo00,Adams,Bob,te,1946,1969", +"AdamCh00,Adams,Charlie,wr,1979,2003" The contents of this file will be mapped to the following - Player domain object: - public class Player implements Serializable { + Player domain object: public class Player implements Serializable { - private String ID; - private String lastName; - private String firstName; - private String position; - private int birthYear; - private int debutYear; + private String ID; + private String lastName; + private String firstName; + private String position; + private int birthYear; + private int debutYear; public String toString() { - - return "PLAYER:ID=" + ID + ",Last Name=" + lastName + - ",First Name=" + firstName + ",Position=" + position + - ",Birth Year=" + birthYear + ",DebutYear=" + - debutYear; + return "PLAYER:ID=" + ID + ",Last Name=" + lastName + + ",First Name=" + firstName + ",Position=" + position + + ",Birth Year=" + birthYear + ",DebutYear=" + + debutYear; } // setters and getters... - } +} In order to map a FieldSet into a @@ -798,39 +760,34 @@ FieldSetMapper that returns players needs to be defined: - - protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> { + protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> { public Player mapFieldSet(FieldSet fieldSet) { - Player player = new Player(); + Player player = new Player(); - player.setID(fieldSet.readString(0)); - player.setLastName(fieldSet.readString(1)); - player.setFirstName(fieldSet.readString(2)); - player.setPosition(fieldSet.readString(3)); - player.setBirthYear(fieldSet.readInt(4)); - player.setDebutYear(fieldSet.readInt(5)); + player.setID(fieldSet.readString(0)); + player.setLastName(fieldSet.readString(1)); + player.setFirstName(fieldSet.readString(2)); + player.setPosition(fieldSet.readString(3)); + player.setBirthYear(fieldSet.readInt(4)); + player.setDebutYear(fieldSet.readInt(5)); - return player; + return player; } - } - +} The file can then be read by correctly constructing a FlatFileItemReader and calling read: - - FlatFileItemReader<Player> itemReader = new FlatFileItemReader<Player>(); - itemReader.setResource(new FileSystemResource("resources/players.csv")); - //DelimitedLineTokenizer defaults to comma as its delimiter - LineMapper<Player> lineMapper = new DefaultLineMapper<Player>(); - lineMapper.setLineTokenizer(new DelimitedLineTokenizer()); - lineMapper.setFieldSetMapper(new PlayerFieldSetMapper()); - itemReader.setLineMapper(lineMapper); - itemReader.open(new ExecutionContext()); - Player player = itemReader.read(); - - + FlatFileItemReader<Player> itemReader = new FlatFileItemReader<Player>(); +itemReader.setResource(new FileSystemResource("resources/players.csv")); +//DelimitedLineTokenizer defaults to comma as its delimiter +LineMapper<Player> lineMapper = new DefaultLineMapper<Player>(); +lineMapper.setLineTokenizer(new DelimitedLineTokenizer()); +lineMapper.setFieldSetMapper(new PlayerFieldSetMapper()); +itemReader.setLineMapper(lineMapper); +itemReader.open(new ExecutionContext()); +Player player = itemReader.read(); Each call to read will return a new Player object from each line in the file. When the end of the file is @@ -849,33 +806,29 @@ readability of the mapping function. First, the column names of all fields in the flat file are injected into the tokenizer: - - tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"}); - + tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"}); a FieldSetMapper can this use this information as follows: - - public class PlayerMapper implements FieldSetMapper<Player> { - public Player mapFieldSet(FieldSet fs) { + public class PlayerMapper implements FieldSetMapper<Player> { + public Player mapFieldSet(FieldSet fs) { - if(fs == null){ - return null; - } + if(fs == null){ + return null; + } - Player player = new Player(); - player.setID(fs.readString("ID")); - player.setLastName(fs.readString("lastName")); - player.setFirstName(fs.readString("firstName")); - player.setPosition(fs.readString("position")); - player.setDebutYear(fs.readInt("debutYear")); - player.setBirthYear(fs.readInt("birthYear")); - - return player; - } - } - + Player player = new Player(); + player.setID(fs.readString("ID")); + player.setLastName(fs.readString("lastName")); + player.setFirstName(fs.readString("firstName")); + player.setPosition(fs.readString("position")); + player.setDebutYear(fs.readInt("debutYear")); + player.setBirthYear(fs.readInt("birthYear")); + + return player; + } +}
@@ -891,17 +844,14 @@ BeanWrapperFieldSetMapper configuration looks like the following: - - <bean id="fieldSetMapper" - class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> + <bean id="fieldSetMapper" + class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> <property name="prototypeBeanName" value="player" /> - </bean> +</bean> - <bean id="player" - class="org.springframework.batch.sample.domain.Player" - scope="prototype" /> - - +<bean id="player" + class="org.springframework.batch.sample.domain.Player" + scope="prototype" /> For each entry in the FieldSet, the mapper will look for a corresponding setter on a new instance of the @@ -921,11 +871,11 @@ organizations that use flat files use fixed length formats. An example fixed length file is below: - UK21341EAH4121131.11customer1 - UK21341EAH4221232.11customer2 - UK21341EAH4321333.11customer3 - UK21341EAH4421434.11customer4 - UK21341EAH4521535.11customer5 + UK21341EAH4121131.11customer1 +UK21341EAH4221232.11customer2 +UK21341EAH4321333.11customer3 +UK21341EAH4421434.11customer4 +UK21341EAH4521535.11customer5 While this looks like one large field, it actually represent 4 distinct fields: @@ -955,14 +905,11 @@ FixedLengthLineTokenizer, each of these lengths must be provided in the form of ranges: - - <bean id="fixedLengthLineTokenizer" - class="org.springframework.batch.io.file.transform.FixedLengthTokenizer"> + <bean id="fixedLengthLineTokenizer" + class="org.springframework.batch.io.file.transform.FixedLengthTokenizer"> <property name="names" value="ISIN, Quantity, Price, Customer" /> <property name="columns" value="1-12, 13-15, 16-20, 21-29" /> - </bean> - - +</bean> Because the FixedLengthLineTokenizer uses the same LineTokenizer interface as discussed @@ -971,23 +918,15 @@ handling its output, such as using the BeanWrapperFieldSetMapper. - It should be noted that supporting the above ranges requires a - specialized property editor be configured anywhere in the - ApplicationContext: - - - <bean id="customEditorConfigurer" - class="org.springframework.beans.factory.config.CustomEditorConfigurer"> - <property name="customEditors"> - <map> - <entry key="org.springframework.batch.item.file.transform.Range[]"> - <bean class="org.springframework.batch.item.file.transform.RangeArrayPropertyEditor" /> - </entry> - </map> - </property> - </bean> - - + + Supporting the above syntax for ranges requires that a + specialized property editor, + RangeArrayPropertyEditor, be configured in + the ApplicationContext. However, this bean + is automatically declared in an + ApplicationContext where the batch + namespace is used. +
@@ -1000,12 +939,9 @@ need to be tokenized differently and mapped to different objects. The following excerpt from a file illustrates this: - - USER;Smith;Peter;;T;20014539;F - LINEA;1044391041ABC037.49G201XX1383.12H - LINEB;2134776319DEF422.99M005LI - - + USER;Smith;Peter;;T;20014539;F +LINEA;1044391041ABC037.49G201XX1383.12H +LINEB;2134776319DEF422.99M005LI In this file we have three types of records, "USER", "LINEA", and "LINEB". A "USER" line corresponds to a User object. "LINEA" and @@ -1016,31 +952,28 @@ individually, but we must specify different LineTokenizer and FieldSetMapper objects so that the - ItemWriter will recieve the correct items. The + ItemWriter will receive the correct items. The PatternMatchingCompositeLineMapper makes this easy by allowing maps of patterns to LineTokenizers and patterns to FieldSetMappers to be configured: - - <bean id="orderFileLineMapper" - class="org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper"> + <bean id="orderFileLineMapper" + class="org.spr...PatternMatchingCompositeLineMapper"> <property name="tokenizers"> - <map> - <entry key="USER*" value-ref="userTokenizer" /> - <entry key="LINEA*" value-ref="lineATokenizer" /> - <entry key="LINEB*" value-ref="lineBTokenizer" /> - </map> + <map> + <entry key="USER*" value-ref="userTokenizer" /> + <entry key="LINEA*" value-ref="lineATokenizer" /> + <entry key="LINEB*" value-ref="lineBTokenizer" /> + </map> </property> <property name="fieldSetMappers"> - <map> - <entry key="USER*" value-ref="userFieldSetMapper" /> - <entry key="LINE*" value-ref="lineFieldSetMapper" /> - </map> + <map> + <entry key="USER*" value-ref="userFieldSetMapper" /> + <entry key="LINE*" value-ref="lineFieldSetMapper" /> + </map> </property> - </bean> - - +</bean> In this example, "LINEA" and "LINEB" have separate LineTokenizers but they both use the same @@ -1053,7 +986,7 @@ allows for two wildcard characters with special meaning: the question mark ("?") will match exactly one character, while the asterisk ("*") will match zero or more characters. Note that in the configuration - above, all paterns end with an asterisk, making them effectively + above, all patterns end with an asterisk, making them effectively prefixes to lines. The PatternMatcher will always match the most specific pattern possible, regardless of the order in the configuration. So if "LINE*" and "LINEA*" were both @@ -1062,10 +995,7 @@ ("*") can serve as a default by matching any line not matched by any other pattern. - - <entry key="*" value-ref="defaultLineTokenizer" /> - - + <entry key="*" value-ref="defaultLineTokenizer" /> There is also a PatternMatchingCompositeLineTokenizer that can @@ -1084,9 +1014,9 @@ exceptions to be thrown. Many flat files are imperfect and contain records that aren't formatted correctly. Many users choose to skip these erroneous lines, logging out the issue, original line, and line - number. These logs can later be inspected manually or or by another - batch job. For this reason, Spring Batch provides a hierarchy of - exceptions for handling parse exceptions: + number. These logs can later be inspected manually or by another batch + job. For this reason, Spring Batch provides a hierarchy of exceptions + for handling parse exceptions: FlatFileParseException and FlatFileFormatException. FlatFileParseException is thrown by the @@ -1110,18 +1040,15 @@ contains the number of tokens encountered, and the number expected: - - tokenizer.setNames(new String[] {"A", "B", "C", "D"}); + tokenizer.setNames(new String[] {"A", "B", "C", "D"}); - try{ +try{ tokenizer.tokenize("a,b,c"); - } - catch(IncorrectTokenCountException e){ +} +catch(IncorrectTokenCountException e){ assertEquals(4, e.getExpectedCount()); assertEquals(3, e.getActualCount()); - } - - +} Because the tokenizer was configured with 4 column names, but only 3 tokens were found in the file, an @@ -1138,18 +1065,17 @@ line length doesn't add up to the widest value of this column, an exception is thrown: - - tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10), new Range(11, 15) }); - try { + tokenizer.setColumns(new Range[] { new Range(1, 5), + new Range(6, 10), + new Range(11, 15) }); +try { tokenizer.tokenize("12345"); fail("Expected IncorrectLineLengthException"); - } - catch (IncorrectLineLengthException ex) { +} +catch (IncorrectLineLengthException ex) { assertEquals(15, ex.getExpectedLength()); assertEquals(5, ex.getActualLength()); - } - - +} The configured ranges for the tokenizer above are: 1-5, 6-10, and 11-15, thus the total length of the line expected is 15. @@ -1163,14 +1089,11 @@ For this reason, validation of line length can be turned off via the 'strict' property: - - tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) }); - tokenizer.setStrict(false); - FieldSet tokens = tokenizer.tokenize("12345"); - assertEquals("12345", tokens.readString(0)); - assertEquals("", tokens.readString(1)); - - + tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) }); +tokenizer.setStrict(false); +FieldSet tokens = tokenizer.tokenize("12345"); +assertEquals("12345", tokens.readString(0)); +assertEquals("", tokens.readString(1)); The above example is almost identical to the one before it, except that tokenizer.setStrict(false) was called. This setting @@ -1200,14 +1123,11 @@ In Spring Batch this is the LineAggregator: - - public interface LineAggregator<T> { + public interface LineAggregator<T> { public String aggregate(T item); - } - - +} The LineAggregator is the opposite of a LineTokenizer. @@ -1226,15 +1146,12 @@ simply assumes that the object is already a string, or that its string representation is acceptable for writing: - - public class PassThroughLineAggregator<T> implements LineAggregator<T> { + public class PassThroughLineAggregator<T> implements LineAggregator<T> { public String aggregate(T item) { - return item.toString(); + return item.toString(); } - } - - +} The above implementation is useful if direct control of creating the string is required, but the advantages of a @@ -1268,26 +1185,20 @@ FlatFileItemWriter expresses this in code: - - public void write(T item) throws Exception { + public void write(T item) throws Exception { write(lineAggregator.aggregate(item) + LINE_SEPARATOR); - } - - +} A simple configuration would look like the following: - - <bean id="itemWriter" - class="org.springframework.batch.io.file.FlatFileItemWriter"> + <bean id="itemWriter" + class="org.springframework.batch.io.file.FlatFileItemWriter"> <property name="resource" value="file:target/test-outputs/20070122.testStream.multilineStep.txt" /> <property name="lineAggregator"> - <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/> + <bean class="org.spr...PassThroughLineAggregator"/> </property> - </bean> - - +</bean>
@@ -1295,9 +1206,9 @@ The above example may be useful for the most basic uses of a writing to a file. However, most users of the - FlatFileItemWriter will will have a domain - object that needs to be written out, and thus must be converted into a - line. In file reading, the following was required: + FlatFileItemWriter will have a domain object + that needs to be written out, and thus must be converted into a line. + In file reading, the following was required: Read one line from the file. @@ -1337,14 +1248,11 @@ FieldExtractor must be written to accomplish the task of turning the item into an array: - - public interface FieldExtractor<T> { + public interface FieldExtractor<T> { Object[] extract(T item); - } - - +} Implementations of the FieldExtractor interface should create an array from the fields of the provided @@ -1371,28 +1279,25 @@ BeanWrapperFieldExtractor As with the BeanWrapperFieldSetMapper - described in the file reading section, it is often preferrable to + described in the file reading section, it is often preferable to configure how to convert a domain object to an object array, rather than writing the conversion yourself. The BeanWrapperFieldExtractor provides just this type of functionality: - - BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>(); - extractor.setNames(new String[] { "first", "last", "born" }); + BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>(); +extractor.setNames(new String[] { "first", "last", "born" }); - String first = "Alan"; - String last = "Turing"; - int born = 1912; +String first = "Alan"; +String last = "Turing"; +int born = 1912; - Name n = new Name(first, last, born); - Object[] values = extractor.extract(n); +Name n = new Name(first, last, born); +Object[] values = extractor.extract(n); - assertEquals(first, values[0]); - assertEquals(last, values[1]); - assertEquals(born, values[2]); - - +assertEquals(first, values[0]); +assertEquals(last, values[1]); +assertEquals(born, values[2]); This extractor implementation has only one required property, the names of the fields to map. Just as the @@ -1415,38 +1320,32 @@ writes out a simple domain object that represents a credit to a customer account: - - public class CustomerCredit { + public class CustomerCredit { private int id; private String name; private BigDecimal credit; //getters and setters removed for clarity - } - - +} Because a domain object is being used, an implementation of the FieldExtractor interface must be provided, along with the delimiter to use: - - <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter"> + <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter"> <property name="resource" ref="outputResource" /> <property name="lineAggregator"> - <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"> - <property name="delimiter" value=","/> - <property name="fieldExtractor"> - <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"> - <property name="names" value="name,credit"/> - </bean> - </property> - </bean> + <bean class="org.spr...DelimitedLineAggregator"> + <property name="delimiter" value=","/> + <property name="fieldExtractor"> + <bean class="org.spr...BeanWrapperFieldExtractor"> + <property name="names" value="name,credit"/> + </bean> + </property> + </bean> </property> - </bean> - - +</bean> In this case, the BeanWrapperFieldExtractor described earlier in @@ -1465,30 +1364,24 @@ Using the same CustomerCredit domain object described above, it can be configured as follows: - - <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter"> + <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter"> <property name="resource" ref="outputResource" /> <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="name,credit" /> - </bean> - </property> - <property name="format" value="%-9s%-2.0f" /> - </bean> + <bean class="org.spr...FormatterLineAggregator"> + <property name="fieldExtractor"> + <bean class="org.spr...BeanWrapperFieldExtractor"> + <property name="names" value="name,credit" /> + </bean> + </property> + <property name="format" value="%-9s%-2.0f" /> + </bean> </property> - </bean> - - +</bean> Most of the above example should look familiar. However, the value of the format property is new: - - <property name="format" value="%-9s%-2.0f" /> - - + <property name="format" value="%-9s%-2.0f" /> The underlying implementation is built using the same Formatter added as part of Java 5. The Java @@ -1554,7 +1447,7 @@ + scale="70" /> Figure 3.1: XML Input @@ -1580,7 +1473,7 @@ + format="PNG" scale="50" /> Figure 3.2: OXM Binding @@ -1597,30 +1490,27 @@ stream. First, lets examine a set of XML records that the StaxEventItemReader can process. - -<?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8"?> <records> - <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> - <isin>XYZ0001</isin> - <quantity>5</quantity> - <price>11.39</price> - <customer>Customer1</customer> - </trade> - <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> - <isin>XYZ0002</isin> - <quantity>2</quantity> - <price>72.99</price> - <customer>Customer2c</customer> - </trade> - <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> - <isin>XYZ0003</isin> - <quantity>9</quantity> - <price>99.99</price> - <customer>Customer3</customer> - </trade> -</records> - - + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0001</isin> + <quantity>5</quantity> + <price>11.39</price> + <customer>Customer1</customer> + </trade> + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0002</isin> + <quantity>2</quantity> + <price>72.99</price> + <customer>Customer2c</customer> + </trade> + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0003</isin> + <quantity>9</quantity> + <price>99.99</price> + <customer>Customer3</customer> + </trade> +</records> To be able to process the XML records the following is needed: @@ -1642,24 +1532,23 @@ - - <bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader"> + <bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader"> <property name="fragmentRootElementName" value="customer" /> <property name="resource" value="data/iosample/input/input.xml" /> <property name="unmarshaller" ref="customerCreditMarshaller" /> - </bean> +</bean> - <bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> +<bean id="customerCreditMarshaller" + class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="aliases"> - <util:map id="aliases"> - <entry key="customer" - value="org.springframework.batch.sample.domain.trade.CustomerCredit" /> - <entry key="price" value="java.math.BigDecimal" /> - <entry key="name" value="java.lang.String" /> - </util:map> + <util:map id="aliases"> + <entry key="customer" + value="org.springframework.batch.sample.domain.CustomerCredit" /> + <entry key="price" value="java.math.BigDecimal" /> + <entry key="name" value="java.lang.String" /> + </util:map> </property> - </bean> - +</bean> Notice that in this example we have chosen to use an XStreamMarshaller that requires an alias passed @@ -1670,24 +1559,23 @@ the map. In the configuration file we can use a Spring configuration utility to describe the required alias as follows: - - <bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader"> + <bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader"> <property name="fragmentRootElementName" value="customer" /> <property name="resource" value="data/iosample/input/input.xml" /> <property name="unmarshaller" ref="customerCreditMarshaller" /> - </bean> +</bean> - <bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> +<bean id="customerCreditMarshaller" + class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="aliases"> - <util:map id="aliases"> - <entry key="customer" - value="org.springframework.batch.sample.domain.trade.CustomerCredit" /> - <entry key="price" value="java.math.BigDecimal" /> - <entry key="name" value="java.lang.String" /> - </util:map> + <util:map id="aliases"> + <entry key="customer" + value="org.springframework.batch.sample.domain.CustomerCredit" /> + <entry key="price" value="java.math.BigDecimal" /> + <entry key="name" value="java.lang.String" /> + </util:map> </property> - </bean> - +</bean> On input the reader reads the XML resource until it recognizes that a new fragment is about to start (by matching the tag name by @@ -1701,35 +1589,33 @@ Java code which uses the injection provided by the Spring configuration: - - StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader() - Resource resource = new ByteArrayResource(xmlResource.getBytes()) + StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader() +Resource resource = new ByteArrayResource(xmlResource.getBytes()) - Map aliases = new HashMap(); - aliases.put("customer","org.springframework.batch.sample.domain.trade.CustomerCredit"); - aliases.put("price","java.math.BigDecimal"); - aliases.put("name","java.lang.String"); - Marshaller marshaller = new XStreamMarshaller(); - marshaller.setAliases(aliases); - xmlStaxEventItemReader.setUnmarshaller(marshaller); - xmlStaxEventItemReader.setResource(resource); - xmlStaxEventItemReader.setFragmentRootElementName("customer"); - xmlStaxEventItemReader.open(new ExecutionContext()); +Map aliases = new HashMap(); +aliases.put("customer","org.springframework.batch.sample.domain.CustomerCredit"); +aliases.put("price","java.math.BigDecimal"); +aliases.put("name","java.lang.String"); +Marshaller marshaller = new XStreamMarshaller(); +marshaller.setAliases(aliases); +xmlStaxEventItemReader.setUnmarshaller(marshaller); +xmlStaxEventItemReader.setResource(resource); +xmlStaxEventItemReader.setFragmentRootElementName("customer"); +xmlStaxEventItemReader.open(new ExecutionContext()); - boolean hasNext = true +boolean hasNext = true - CustomerCredit credit = null; +CustomerCredit credit = null; - while (hasNext) { - credit = xmlStaxEventItemReader.read(); - if (credit == null) { - hasNext = false; - } else { - println trade; - } - } - - +while (hasNext) { + credit = xmlStaxEventItemReader.read(); + if (credit == null) { + hasNext = false; + } + else { + System.out.println(credit); + } +}
@@ -1747,15 +1633,12 @@ MarshallingEventWriterSerializer. The Spring configuration for this setup looks as follows: - - <bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter"> + <bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter"> <property name="resource" ref="outputResource" /> <property name="marshaller" ref="customerCreditMarshaller" /> <property name="rootTagName" value="customers" /> <property name="overwriteOutput" value="true" /> - </bean> - - +</bean> The configuration sets up the three required properties and optionally sets the overwriteOutput=true, mentioned earlier in the @@ -1763,45 +1646,43 @@ should be noted the marshaller used for the writer is the exact same as the one used in the reading example from earlier in the chapter: - - <bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> + <bean id="customerCreditMarshaller" + class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="aliases"> - <util:map id="aliases"> - <entry key="customer" - value="org.springframework.batch.sample.domain.trade.CustomerCredit" /> - <entry key="price" value="java.math.BigDecimal" /> - <entry key="name" value="java.lang.String" /> - </util:map> + <util:map id="aliases"> + <entry key="customer" + value="org.springframework.batch.sample.domain.CustomerCredit" /> + <entry key="price" value="java.math.BigDecimal" /> + <entry key="name" value="java.lang.String" /> + </util:map> </property> - </bean> - +</bean> To summarize with a Java example, the following code illustrates all of the points discussed, demonstrating the programmatic setup of the required properties: - StaxEventItemWriter staxItemWriter = new StaxEventItemWriter() - FileSystemResource resource = new FileSystemResource(File.createTempFile("StaxEventWriterOutputSourceTests", "xml")) + StaxEventItemWriter staxItemWriter = new StaxEventItemWriter() +FileSystemResource resource = new FileSystemResource("data/outputFile.xml") - Map aliases = new HashMap(); - aliases.put("customer","org.springframework.batch.sample.domain.trade.CustomerCredit"); - aliases.put("price","java.math.BigDecimal"); - aliases.put("name","java.lang.String"); - Marshaller marshaller = new XStreamMarshaller(); - marshaller.setAliases(aliases); +Map aliases = new HashMap(); +aliases.put("customer","org.springframework.batch.sample.domain.CustomerCredit"); +aliases.put("price","java.math.BigDecimal"); +aliases.put("name","java.lang.String"); +Marshaller marshaller = new XStreamMarshaller(); +marshaller.setAliases(aliases); - staxItemWriter.setResource(resource); - staxItemWriter.setMarshaller(marshaller); - staxItemWriter.setRootTagName("trades"); - staxItemWriter.setOverwriteOutput(true); +staxItemWriter.setResource(resource); +staxItemWriter.setMarshaller(marshaller); +staxItemWriter.setRootTagName("trades"); +staxItemWriter.setOverwriteOutput(true); - ExecutionContext executionContext = new ExecutionContext(); - staxItemWriter.open(executionContext); - CustomerCredit Credit = new CustomerCredit(); - trade.setPrice(11.39); - credit.setName("Customer1"); - staxItemWriter.write(trade); - +ExecutionContext executionContext = new ExecutionContext(); +staxItemWriter.open(executionContext); +CustomerCredit Credit = new CustomerCredit(); +trade.setPrice(11.39); +credit.setName("Customer1"); +staxItemWriter.write(trade);
@@ -1821,13 +1702,10 @@ MuliResourceItemReader can be used to read in both files by using wildcards: - - <bean id="multiResourceReader" class="org.springframework.batch.item.file.MultiResourceItemReader"> - <property name="resources" value="classpath:data/multiResourceJob/input/file-*.txt" /> + <bean id="multiResourceReader" class="org.spr...MultiResourceItemReader"> + <property name="resources" value="classpath:data/input/file-*.txt" /> <property name="delegate" ref="flatFileItemReader" /> - </bean> - - +</bean> The referenced delegate is a simple FlatFileItemReader. The above configuration will @@ -1884,12 +1762,12 @@ + scale="65" /> + scale="40" /> @@ -1914,18 +1792,18 @@ DataSource. The following database schema will be used as an example: - CREATE TABLE CUSTOMER ( + CREATE TABLE CUSTOMER ( ID BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(45), CREDIT FLOAT - ); +); Many people prefer to use a domain object for each row, so we'll use an implementation of the RowMapper interface to map a CustomerCredit object: - public class CustomerCreditRowMapper implements RowMapper { + public class CustomerCreditRowMapper implements RowMapper { public static final String ID_COLUMN = "id"; public static final String NAME_COLUMN = "name"; @@ -1951,12 +1829,10 @@ CUSTOMER database. The first example will be using JdbcTemplate: - - //For simplicity sake, assume a dataSource has already been obtained - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER", new CustomerCreditRowMapper()); - - + //For simplicity sake, assume a dataSource has already been obtained +JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); +List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER", + new CustomerCreditRowMapper()); After running this code snippet the customerCredits list will contain 1,000 CustomerCredit objects. In the @@ -1967,22 +1843,19 @@ contrast this with the approach of the JdbcCursorItemReader: - - JdbcCursorItemReader itemReader = new JdbcCursorItemReader(); - itemReader.setDataSource(dataSource); - itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER"); - itemReader.setRowMapper(new CustomerCreditRowMapper()); - int counter = 0; - ExecutionContext executionContext = new ExecutionContext(); - itemReader.open(executionContext); - Object customerCredit = new Object(); - while(customerCredit != null){ + JdbcCursorItemReader itemReader = new JdbcCursorItemReader(); +itemReader.setDataSource(dataSource); +itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER"); +itemReader.setRowMapper(new CustomerCreditRowMapper()); +int counter = 0; +ExecutionContext executionContext = new ExecutionContext(); +itemReader.open(executionContext); +Object customerCredit = new Object(); +while(customerCredit != null){ customerCredit = itemReader.read(); counter++; - } - itemReader.close(executionContext); - - +} +itemReader.close(executionContext); After running this code snippet the counter will equal 1,000. If the code above had put the returned customerCredit into a list, the @@ -1998,16 +1871,13 @@ configured for injection into a Spring Batch Step: - - <bean id="itemReader" class="org.springframework.batch.item.database.JdbcCursorItemReader"> + <bean id="itemReader" class="org.spr...JdbcCursorItemReader"> <property name="dataSource" ref="dataSource"/> <property name="sql" value="select ID, NAME, CREDIT from CUSTOMER"/> <property name="rowMapper"> - <bean class="org.springframework.batch.sample.domain.trade.internal.CustomerCreditRowMapper"/> + <bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/> </property> - </bean> - - +</bean>
Additional Properties @@ -2146,23 +2016,20 @@ configuration using the same 'customer credit' example as the JDBC reader: - - HibernateCursorItemReader itemReader = new HibernateCursorItemReader(); - itemReader.setQueryString("from CustomerCredit"); - //For simplicity sake, assume sessionFactory already obtained. - itemReader.setSessionFactory(sessionFactory); - itemReader.setUseStatelessSession(true); - int counter = 0; - ExecutionContext executionContext = new ExecutionContext(); - itemReader.open(executionContext); - Object customerCredit = new Object(); - while(customerCredit != null){ + HibernateCursorItemReader itemReader = new HibernateCursorItemReader(); +itemReader.setQueryString("from CustomerCredit"); +//For simplicity sake, assume sessionFactory already obtained. +itemReader.setSessionFactory(sessionFactory); +itemReader.setUseStatelessSession(true); +int counter = 0; +ExecutionContext executionContext = new ExecutionContext(); +itemReader.open(executionContext); +Object customerCredit = new Object(); +while(customerCredit != null){ customerCredit = itemReader.read(); counter++; - } - itemReader.close(executionContext); - - +} +itemReader.close(executionContext); This configured ItemReader will return CustomerCredit objects in the exact same manner @@ -2175,14 +2042,11 @@ JdbcCursorItemReader, configuration is straightforward: - - <bean id="itemReader" - class="org.springframework.batch.item.database.HibernateCursorItemReader"> + <bean id="itemReader" + class="org.springframework.batch.item.database.HibernateCursorItemReader"> <property name="sessionFactory" ref="sessionFactory" /> <property name="queryString" value="from CustomerCredit" /> - </bean> - - +</bean>
@@ -2225,26 +2089,24 @@ Below is an example configuration using a similar 'customer credit' example as the cursor based ItemReaders above: - <bean id="itemReader" - class="org.springframework.batch.item.database.JdbcPagingItemReader"> - <property name="dataSource" ref="dataSource"/> - <property name="queryProvider"> - <bean class="org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean"> - <property name="selectClause" value="select id, name, credit"/> - <property name="fromClause" value="from customer"/> - <property name="whereClause" value="where status=:status"/> - <property name="sortKey" value="id"/> - </bean> - </property> - <property name="parameterValues"> - <map> - <entry key="status" value="NEW"/> - </map> - </property> - <property name="pageSize" value="1000"/> - <property name="rowMapper" ref="customerMapper"/> - </bean> - + <bean id="itemReader" class="org.spr...JdbcPagingItemReader"> + <property name="dataSource" ref="dataSource"/> + <property name="queryProvider"> + <bean class="org.spr...SqlPagingQueryProviderFactoryBean"> + <property name="selectClause" value="select id, name, credit"/> + <property name="fromClause" value="from customer"/> + <property name="whereClause" value="where status=:status"/> + <property name="sortKey" value="id"/> + </bean> + </property> + <property name="parameterValues"> + <map> + <entry key="status" value="NEW"/> + </map> + </property> + <property name="pageSize" value="1000"/> + <property name="rowMapper" ref="customerMapper"/> +</bean> This configured ItemReader will return CustomerCredit objects using the @@ -2283,13 +2145,11 @@ is an example configuration using the same 'customer credit' example as the JDBC reader above: - <bean id="itemReader" - class="org.springframework.batch.item.database.JpaPagingItemReader"> + <bean id="itemReader" class="org.spr...JpaPagingItemReader"> <property name="entityManagerFactory" ref="entityManagerFactory"/> <property name="queryString" value="select c from CustomerCredit c"/> <property name="pageSize" value="1000"/> - </bean> - +</bean> This configured ItemReader will return CustomerCredit objects in the exact same manner @@ -2313,23 +2173,20 @@ IbatisPagingItemReader reading CustomerCredits as in the examples above: - <bean id="itemReader" - class="org.springframework.batch.item.database.IbatisPagingItemReader"> + <bean id="itemReader" class="org.spr...IbatisPagingItemReader"> <property name="sqlMapClient" ref="sqlMapClient"/> <property name="queryId" value="getPagedCustomerCredits"/> <property name="pageSize" value="1000"/> - </bean> - +</bean> The IbatisPagingItemReader configuration above references an IBATIS query called "getPagedCustomerCredits". Here is an example of what that query should look like for MySQL. - <select id="getPagedCustomerCredits" resultMap="customerCreditResult"> - select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize# - </select> - + <select id="getPagedCustomerCredits" resultMap="customerCreditResult"> + select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize# +</select> The _skiprows and _pagesize variables are provided by the @@ -2339,11 +2196,11 @@ is an example for Oracle (unfortunately we need to use CDATA for some operators since this belongs in an XML document): - <select id="getPagedCustomerCredits" resultMap="customerCreditResult"> + <select id="getPagedCustomerCredits" resultMap="customerCreditResult"> select * from ( - select * from ( - select t.id, t.name, t.credit, ROWNUM ROWNUM_ from customer t order by id - ) where ROWNUM_ <![CDATA[ > ]]> ( #_page# * #_pagesize# ) + select * from ( + select t.id, t.name, t.credit, ROWNUM ROWNUM_ from customer t order by id + ) where ROWNUM_ <![CDATA[ > ]]> ( #_page# * #_pagesize# ) ) where ROWNUM <![CDATA[ <= ]]> #_pagesize# </select> @@ -2375,7 +2232,8 @@ - + @@ -2401,11 +2259,12 @@ - + - @@ -2441,12 +2300,12 @@ standard Spring method invoking the delegate pattern and are fairly simple to set up. Below is an example of the reader: - <bean id="itemReader" class="org.springframework.batch.item.adapter.ItemReaderAdapter"> + <bean id="itemReader" class="org.springframework.batch.item.adapter.ItemReaderAdapter"> <property name="targetObject" ref="fooService" /> <property name="targetMethod" value="generateFoo" /> - </bean> +</bean> - <bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> +<bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> One important point to note is that the contract of the targetMethod must be the same as the contract for read: when @@ -2457,12 +2316,12 @@ ItemWriter. The ItemWriter implementation is equally as simple: - <bean id="itemWriter" class="org.springframework.batch.item.adapter.ItemWriterAdapter"> + <bean id="itemWriter" class="org.springframework.batch.item.adapter.ItemWriterAdapter"> <property name="targetObject" ref="fooService" /> <property name="targetMethod" value="processFoo" /> - </bean> +</bean> - <bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> +<bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> @@ -2486,48 +2345,44 @@ rather provides a very simple interface that can be implemented by any number of frameworks: - - public interface Validator { + public interface Validator { void validate(Object value) throws ValidationException; - } - - +} The contract is that the validate method will throw an exception if the object is invalid, and return normally if it is valid. Spring Batch provides an out of the box ItemProcessor: - <bean class="org.springframework.batch.item.validator.ValidatingItemProcessor"> + <bean class="org.springframework.batch.item.validator.ValidatingItemProcessor"> <property name="validator" ref="validator" /> - </bean> +</bean> - <bean id="validator" - class="org.springframework.batch.item.validator.SpringValidator"> +<bean id="validator" + class="org.springframework.batch.item.validator.SpringValidator"> <property name="validator"> - <bean id="orderValidator" - class="org.springmodules.validation.valang.ValangValidator"> - <property name="valang"> - <value> - <![CDATA[ - { orderId : ? > 0 AND ? <= 9999999999 : 'Incorrect order ID' : 'error.order.id' } - { totalLines : ? = size(lineItems) : 'Bad count of order lines' - : 'error.order.lines.badcount'} - { customer.registered : customer.businessCustomer = FALSE OR ? = TRUE - : 'Business customer must be registered' - : 'error.customer.registration'} - { customer.companyName : customer.businessCustomer = FALSE OR ? HAS TEXT - : 'Company name for business customer is mandatory' - :'error.customer.companyname'} - ]]> - </value> - </property> - </bean> + <bean id="orderValidator" + class="org.springmodules.validation.valang.ValangValidator"> + <property name="valang"> + <value> + <![CDATA[ + { orderId : ? > 0 AND ? <= 9999999999 : 'Incorrect order ID' : 'error.order.id' } + { totalLines : ? = size(lineItems) : 'Bad count of order lines' + : 'error.order.lines.badcount'} + { customer.registered : customer.businessCustomer = FALSE OR ? = TRUE + : 'Business customer must be registered' + : 'error.customer.registration'} + { customer.companyName : customer.businessCustomer = FALSE OR ? HAS TEXT + : 'Company name for business customer is mandatory' + :'error.customer.companyname'} + ]]> + </value> + </property> + </bean> </property> - </bean> - +</bean> This simple example shows a simple ValangValidator that is used to validate an order @@ -2554,27 +2409,23 @@ restart. For this reason, all readers and writers include the 'saveState' property: - - <bean id="playerSummarizationSource" - class="org.springframework.batch.item.database.JdbcCursorItemReader"> + <bean id="playerSummarizationSource" class="org.spr...JdbcCursorItemReader"> <property name="dataSource" ref="dataSource" /> <property name="rowMapper"> - <bean class="org.springframework.batch.sample.mapping.PlayerSummaryMapper" /> + <bean class="org.springframework.batch.sample.PlayerSummaryMapper" /> </property> <property name="saveState" value="false" /> <property name="sql"> - <value> - SELECT games.player_id, games.year_no, SUM(COMPLETES), - SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD), - SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS), - SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD) - from games, players where players.player_id = - games.player_id group by games.player_id, games.year_no - </value> + <value> + SELECT games.player_id, games.year_no, SUM(COMPLETES), + SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD), + SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS), + SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD) + from games, players where players.player_id = + games.player_id group by games.player_id, games.year_no + </value> </property> - </bean> - - +</bean> The ItemReader configured above will not make any entries in the ExecutionContext for any @@ -2605,42 +2456,39 @@ basic contract of ItemReader, read: - - public class CustomItemReader<T> implements ItemReader<T>{ + public class CustomItemReader<T> implements ItemReader<T>{ List<T> items; public CustomItemReader(List<T> items) { - this.items = items; + this.items = items; } public T read() throws Exception, UnexpectedInputException, NoWorkFoundException, ParseException { - if (!items.isEmpty()) { - return items.remove(0); - } - return null; + if (!items.isEmpty()) { + return items.remove(0); + } + return null; } - } - - +} This very simple class takes a list of items, and returns them one at a time, removing each from the list. When the list is empty, it returns null, thus satisfying the most basic requirements of an ItemReader, as illustrated below: - List<String> items = new ArrayList<String>(); - items.add("1"); - items.add("2"); - items.add("3"); + List<String> items = new ArrayList<String>(); +items.add("1"); +items.add("2"); +items.add("3"); - ItemReader itemReader = new CustomItemReader<String>(items); - assertEquals("1", itemReader.read()); - assertEquals("2", itemReader.read()); - assertEquals("3", itemReader.read()); - assertNull(itemReader.read()); +ItemReader itemReader = new CustomItemReader<String>(items); +assertEquals("1", itemReader.read()); +assertEquals("2", itemReader.read()); +assertEquals("3", itemReader.read()); +assertNull(itemReader.read());
Making the <classname>ItemReader</classname> @@ -2661,41 +2509,41 @@ <para>If you do need to store state, then the <classname>ItemStream</classname> interface should be used:</para> - <programlisting> public class CustomItemReader<T> implements ItemReader<T>, ItemStream { + <programlisting>public class CustomItemReader<T> implements ItemReader<T>, ItemStream { List<T> items; int currentIndex = 0; private static final String CURRENT_INDEX = "current.index"; public CustomItemReader(List<T> items) { - this.items = items; + this.items = items; } public T read() throws Exception, UnexpectedInputException, ParseException { - if (currentIndex < items.size()) { - return items.get(currentIndex++); - } + if (currentIndex < items.size()) { + return items.get(currentIndex++); + } - return null; + return null; } public void open(ExecutionContext executionContext) throws ItemStreamException { - if(executionContext.containsKey(CURRENT_INDEX)){ - currentIndex = new Long(executionContext.getLong(CURRENT_INDEX)).intValue(); - } - else{ - currentIndex = 0; - } + if(executionContext.containsKey(CURRENT_INDEX)){ + currentIndex = new Long(executionContext.getLong(CURRENT_INDEX)).intValue(); + } + else{ + currentIndex = 0; + } } public void update(ExecutionContext executionContext) throws ItemStreamException { - executionContext.putLong(CURRENT_INDEX, new Long(currentIndex).longValue()); - }; + executionContext.putLong(CURRENT_INDEX, new Long(currentIndex).longValue()); + } public void close() throws ItemStreamException {} - }</programlisting> +}</programlisting> <para>On each call to the <classname>ItemStream</classname> <methodname>update</methodname> method, the current index of the @@ -2708,19 +2556,19 @@ fairly trivial example, but it still meets the general contract:</para> - <programlisting> ExecutionContext executionContext = new ExecutionContext(); - ((ItemStream)itemReader).open(executionContext); - assertEquals("1", itemReader.read()); - ((ItemStream)itemReader).update(executionContext); + <programlisting>ExecutionContext executionContext = new ExecutionContext(); +((ItemStream)itemReader).open(executionContext); +assertEquals("1", itemReader.read()); +((ItemStream)itemReader).update(executionContext); - List<String> items = new ArrayList<String>(); - items.add("1"); - items.add("2"); - items.add("3"); - itemReader = new CustomItemReader<String>(items); +List<String> items = new ArrayList<String>(); +items.add("1"); +items.add("2"); +items.add("3"); +itemReader = new CustomItemReader<String>(items); - ((ItemStream)itemReader).open(executionContext); - assertEquals("2", itemReader.read());</programlisting> +((ItemStream)itemReader).open(executionContext); +assertEquals("2", itemReader.read());</programlisting> <para>Most ItemReaders have much more sophisticated restart logic. The <classname>JdbcCursorItemReader</classname>, for example, stores the @@ -2754,18 +2602,18 @@ <classname>List</classname> will be used in order to keep the example as simple as possible:</para> - <programlisting> public class CustomItemWriter<T> implements ItemWriter<T> { + <programlisting>public class CustomItemWriter<T> implements ItemWriter<T> { List<T> output = TransactionAwareProxyFactory.createTransactionalList(); public void write(List<? extends T> items) throws Exception { - output.addAll(items); + output.addAll(items); } public List<T> getOutput() { - return output; + return output; } - }</programlisting> +}</programlisting> <section> <title>Making the <classname>ItemWriter</classname> diff --git a/src/site/docbook/reference/step.xml b/src/site/docbook/reference/step.xml index a415c0885..b6d04d320 100644 --- a/src/site/docbook/reference/step.xml +++ b/src/site/docbook/reference/step.xml @@ -19,12 +19,11 @@ <mediaobject> <imageobject role="html"> - <imagedata align="center" fileref="images/step.png" scale="75" width="" /> + <imagedata align="center" fileref="images/step.png" scale="50" /> </imageobject> <imageobject role="fo"> - <imagedata align="center" contentwidth="480" fileref="images/step.png" - scale="50" width="75%" /> + <imagedata align="center" fileref="images/step.png" scale="30" /> </imageobject> </mediaobject> @@ -43,13 +42,12 @@ <mediaobject> <imageobject role="html"> <imagedata align="center" - fileref="images/chunk-oriented-processing.png" scale="75" - width="" /> + fileref="images/chunk-oriented-processing.png" scale="75" /> </imageobject> <imageobject role="fo"> <imagedata align="center" - fileref="images/chunk-oriented-processing.png" width="75%" /> + fileref="images/chunk-oriented-processing.png" scale="75" /> </imageobject> </mediaobject> @@ -477,8 +475,8 @@ itemWriter.write(items);</programlisting> records are logged as well, which will be covered later when discussing listeners.<programlisting><step id="step1"> <tasklet> - <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" <emphasis - role="bold">skip-limit="10"</emphasis>> + <chunk reader="flatFileItemReader" writer="itemWriter" + commit-interval="10" <emphasis role="bold">skip-limit="10"</emphasis>> <emphasis role="bold"><skippable-exception-classes> org.springframework.batch.item.file.FlatFileParseException </skippable-exception-classes></emphasis> @@ -505,8 +503,8 @@ itemWriter.write(items);</programlisting> identify which exceptions should cause failure and skip everything else:<programlisting><step id="step1"> <tasklet> - <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" <emphasis - role="bold">skip-limit="10"</emphasis>> + <chunk reader="flatFileItemReader" writer="itemWriter" + commit-interval="10" <emphasis role="bold">skip-limit="10"</emphasis>> <emphasis role="bold"> <skippable-exception-classes> java.lang.Exception </skippable-exception-classes> @@ -539,8 +537,8 @@ itemWriter.write(items);</programlisting> <programlisting><step id="step1"> <tasklet> - <chunk reader="itemReader" writer="itemWriter" commit-interval="2" <emphasis - role="bold">retry-limit="3"</emphasis>> + <chunk reader="itemReader" writer="itemWriter" + commit-interval="2" <emphasis role="bold">retry-limit="3"</emphasis>> <emphasis role="bold"><retryable-exception-classes> org.springframework.dao.DeadlockLoserDataAccessException </retryable-exception-classes></emphasis> @@ -612,7 +610,9 @@ itemWriter.write(items);</programlisting> <programlisting><step id="step1"> <tasklet> <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/> - <transaction-attributes isolation="DEFAULT" propagation="REQUIRED" timeout="30"/> + <transaction-attributes isolation="DEFAULT" + propagation="REQUIRED" + timeout="30"/> </tasklet> </step></programlisting> </section> @@ -1115,12 +1115,12 @@ itemWriter.write(items);</programlisting> <mediaobject> <imageobject role="html"> <imagedata align="center" fileref="images/sequential-flow.png" - scale="80" width="40%" /> + scale="80" /> </imageobject> <imageobject role="fo"> <imagedata align="center" fileref="images/sequential-flow.png" - width="40%" /> + scale="40" /> </imageobject> </mediaobject> @@ -1298,8 +1298,9 @@ itemWriter.write(items);</programlisting> <programlisting>public class SkipCheckingListener extends StepExecutionListenerSupport { public ExitStatus afterStep(StepExecution stepExecution) { - if (!stepExecution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode()) - && stepExecution.getSkipCount() > 0) { + String exitCode = stepExecution.getExitStatus().getExitCode(); + if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && + stepExecution.getSkipCount() > 0) { return new ExitStatus("COMPLETED WITH SKIPS"); } else {