diff --git a/spring-batch-samples/.springBeans b/spring-batch-samples/.springBeans
index 7c73069e8..367ebfe4a 100644
--- a/spring-batch-samples/.springBeans
+++ b/spring-batch-samples/.springBeans
@@ -1,7 +1,7 @@
1
-
+
@@ -42,6 +42,7 @@
src/main/resources/org/springframework/batch/sample/config/common-context.xml
src/main/resources/jobs/taskletJob.xml
src/main/resources/jobs/headerFooterSample.xml
+ src/main/resources/jobs/customerFilterJob.xml
@@ -357,5 +358,17 @@
src/main/resources/simple-job-launcher-context.xml
+
+
+ true
+ false
+
+ src/main/resources/jobs/customerFilterJob.xml
+ src/main/resources/simple-job-launcher-context.xml
+ src/main/resources/data-source-context.xml
+ src/main/resources/data-source-context-init.xml
+ src/main/resources/org/springframework/batch/sample/config/common-context.xml
+
+
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CompositeCustomerUpdateLineTokenizer.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CompositeCustomerUpdateLineTokenizer.java
new file mode 100644
index 000000000..d9ddc01f1
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CompositeCustomerUpdateLineTokenizer.java
@@ -0,0 +1,61 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import org.springframework.batch.item.file.mapping.FieldSet;
+import org.springframework.batch.item.file.transform.LineTokenizer;
+
+/**
+ * Composite {@link LineTokenizer} that delegates the tokenization of a line to one of two potential
+ * tokenizers. The file format in this case uses one character, either F, A, U, or D to indicate
+ * whether or not the line is an a footer record, or a customer add, update, or delete, and
+ * will delegate accordingly.
+ *
+ * @author Lucas Ward
+ * @since 2.0
+ */
+public class CompositeCustomerUpdateLineTokenizer implements LineTokenizer {
+
+ private LineTokenizer customerTokenizer;
+ private LineTokenizer footerTokenizer;
+
+ /* (non-Javadoc)
+ * @see org.springframework.batch.item.file.transform.LineTokenizer#tokenize(java.lang.String)
+ */
+ public FieldSet tokenize(String line) {
+
+ if(line.charAt(0) == 'F'){
+ //line starts with F, so the footer tokenizer should tokenize it.
+ return footerTokenizer.tokenize(line);
+ }
+ else if(line.charAt(0) == 'A' || line.charAt(0) == 'U' || line.charAt(0) == 'D'){
+ //line starts with A,U, or D, so it must be a customer operation.
+ return customerTokenizer.tokenize(line);
+ }
+ else{
+ //If the line doesn't start with any of the characters above, it must obviously be invalid.
+ throw new IllegalArgumentException("Invalid line encountered for tokenizing: " + line);
+ }
+ }
+
+ /**
+ * Set the {@link LineTokenizer} that will be used to tokenize any lines that begin with
+ * A, U, or D, and are thus a customer operation.
+ *
+ * @param customerTokenizer
+ */
+ public void setCustomerTokenizer(LineTokenizer customerTokenizer) {
+ this.customerTokenizer = customerTokenizer;
+ }
+
+ /**
+ * Set the {@link LineTokenizer} that will be used to tokenize any lines that being with
+ * F and is thus a footer record.
+ *
+ * @param footerTokenizer
+ */
+ public void setFooterTokenizer(LineTokenizer footerTokenizer) {
+ this.footerTokenizer = footerTokenizer;
+ }
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerDao.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerDao.java
new file mode 100644
index 000000000..c6535c7e0
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerDao.java
@@ -0,0 +1,19 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import java.math.BigDecimal;
+
+/**
+ * @author Lucas Ward
+ *
+ */
+public interface CustomerDao {
+
+ CustomerCredit getCustomerByName(String name);
+
+ void insertCustomer(String name, BigDecimal credit);
+
+ void updateCustomer(String name, BigDecimal credit);
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerOperation.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerOperation.java
new file mode 100644
index 000000000..cfc5037dd
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerOperation.java
@@ -0,0 +1,45 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enum representing on of 3 possible actions on a customer update:
+ * Add, update, or delete
+ *
+ * @author Lucas Ward
+ *
+ */
+public enum CustomerOperation {
+ ADD('A'), UPDATE('U'), DELETE('D');
+
+ private final char code;
+ private static final Map codeMap;
+
+ private CustomerOperation(char code) {
+ this.code = code;
+ }
+
+ static{
+ codeMap = new HashMap();
+ for(CustomerOperation operation:values()){
+ codeMap.put(operation.getCode(), operation);
+ }
+ }
+
+ public static CustomerOperation fromCode(char code){
+ if(codeMap.containsKey(code)){
+ return codeMap.get(code);
+ }
+ else{
+ throw new IllegalArgumentException("Invalid code: [" + code + "]");
+ }
+ }
+
+ public char getCode() {
+ return code;
+ }
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdate.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdate.java
new file mode 100644
index 000000000..fbdfb8ffe
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdate.java
@@ -0,0 +1,46 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import java.math.BigDecimal;
+
+/**
+ * Immutable Value Object representing an update to the customer as stored in the database.
+ * This object has the customer name, credit ammount, and the operation to be performed
+ * on them. In the case of an add, a new customer will be entered with the appropriate
+ * credit. In the case of an update, the customer's credit is considered an absolute update.
+ * Deletes are currently not supported, but can still be read in from a file.
+ *
+ * @author Lucas Ward
+ * @since 2.0
+ */
+public class CustomerUpdate {
+
+ private final CustomerOperation operation;
+ private final String customerName;
+ private final BigDecimal credit;
+
+ public CustomerUpdate(CustomerOperation operation, String customerName, BigDecimal credit) {
+ this.operation = operation;
+ this.customerName = customerName;
+ this.credit = credit;
+ }
+
+ public CustomerOperation getOperation() {
+ return operation;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public BigDecimal getCredit() {
+ return credit;
+ }
+
+ @Override
+ public String toString() {
+ return "Customer Update, name: [" + customerName + "], operation: [" + operation + "], credit: [" + credit + "]";
+ }
+}
\ No newline at end of file
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateFieldSetMapper.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateFieldSetMapper.java
new file mode 100644
index 000000000..942952672
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateFieldSetMapper.java
@@ -0,0 +1,52 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import java.math.BigDecimal;
+
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.listener.StepExecutionListenerSupport;
+import org.springframework.batch.item.file.mapping.FieldSet;
+import org.springframework.batch.item.file.mapping.FieldSetMapper;
+
+/**
+ * {@link FieldSetMapper} for mapping the
+ *
+ * @author Lucas Ward
+ *
+ */
+public class CustomerUpdateFieldSetMapper extends StepExecutionListenerSupport implements FieldSetMapper {
+
+ StepExecution stepExecution;
+
+ public CustomerUpdate mapLine(FieldSet fs) {
+
+ char code = fs.readChar(0);
+ if(code == 'F'){
+ long customerUpdateTotal = stepExecution.getReadCount();
+ long fileUpdateTotal = fs.readLong(1);
+ if(customerUpdateTotal != fileUpdateTotal){
+ throw new IllegalStateException("The total number of customer updates in the file footer does not match the " +
+ "number entered File footer total: [" + fileUpdateTotal + "] Total encountered during processing: [" +
+ customerUpdateTotal + "]");
+ }
+ else{
+ //return null, because the footer indicates an end of processing.
+ return null;
+ }
+ }
+
+ CustomerOperation operation = CustomerOperation.fromCode(code);
+ String name = fs.readString(1);
+ BigDecimal credit = fs.readBigDecimal(2);
+
+ return new CustomerUpdate(operation, name, credit);
+ }
+
+ @Override
+ public void beforeStep(StepExecution stepExecution) {
+
+ this.stepExecution = stepExecution;
+ }
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateProcessor.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateProcessor.java
new file mode 100644
index 000000000..23e052d21
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateProcessor.java
@@ -0,0 +1,61 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import static org.springframework.batch.sample.domain.trade.CustomerOperation.*;
+
+import org.springframework.batch.item.ItemProcessor;
+
+/**
+ * @author Lucas Ward
+ *
+ */
+public class CustomerUpdateProcessor implements ItemProcessor{
+
+ CustomerDao customerDao;
+ InvalidCustomerLogger invalidCustomerLogger;
+
+ public CustomerUpdate process(CustomerUpdate item) throws Exception {
+
+ if(item.getOperation() == DELETE){
+ //delete is not supported
+ invalidCustomerLogger.log(item);
+ return null;
+ }
+
+ CustomerCredit customerCredit = customerDao.getCustomerByName(item.getCustomerName());
+
+ if(item.getOperation() == ADD && customerCredit == null){
+ return item;
+ }
+ else if(item.getOperation() == ADD && customerCredit != null){
+ //veto processing
+ invalidCustomerLogger.log(item);
+ return null;
+ }
+
+ if(item.getOperation() == UPDATE && customerCredit != null){
+ return item;
+ }
+ else if(item.getOperation() == UPDATE && customerCredit == null){
+ //veto processing
+ invalidCustomerLogger.log(item);
+ return null;
+ }
+
+ //if an item makes it through all these checks it can be assumed to be bad, logged, and skipped
+ invalidCustomerLogger.log(item);
+ return null;
+ }
+
+ public void setCustomerDao(CustomerDao customerDao) {
+ this.customerDao = customerDao;
+ }
+
+ public void setInvalidCustomerLogger(
+ InvalidCustomerLogger invalidCustomerLogger) {
+ this.invalidCustomerLogger = invalidCustomerLogger;
+ }
+
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateWriter.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateWriter.java
new file mode 100644
index 000000000..ed8ebc819
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/CustomerUpdateWriter.java
@@ -0,0 +1,33 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+import java.util.List;
+
+import org.springframework.batch.item.ItemWriter;
+
+/**
+ * @author Lucas Wward
+ *
+ */
+public class CustomerUpdateWriter implements ItemWriter {
+
+ private CustomerDao customerDao;
+
+ public void write(List extends CustomerUpdate> items) throws Exception {
+ for(CustomerUpdate customerUpdate : items){
+ if(customerUpdate.getOperation() == CustomerOperation.ADD){
+ customerDao.insertCustomer(customerUpdate.getCustomerName(), customerUpdate.getCredit());
+ }
+ else if(customerUpdate.getOperation() == CustomerOperation.UPDATE){
+ customerDao.updateCustomer(customerUpdate.getCustomerName(), customerUpdate.getCredit());
+ }
+ }
+ }
+
+ public void setCustomerDao(CustomerDao customerDao) {
+ this.customerDao = customerDao;
+ }
+
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/InvalidCustomerLogger.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/InvalidCustomerLogger.java
new file mode 100644
index 000000000..c1025b98d
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/InvalidCustomerLogger.java
@@ -0,0 +1,18 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade;
+
+/**
+ * Interface for logging invalid customers. Customers may need to be logged because
+ * they already existed when attempted to be added. Or a non existent customer was
+ * updated.
+ *
+ * @author Lucas Ward
+ *
+ */
+public interface InvalidCustomerLogger {
+
+ void log(CustomerUpdate customerUpdate);
+
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/CommonsLoggingInvalidCustomerLogger.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/CommonsLoggingInvalidCustomerLogger.java
new file mode 100644
index 000000000..84380d87c
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/CommonsLoggingInvalidCustomerLogger.java
@@ -0,0 +1,28 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade.internal;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.batch.core.launch.support.CommandLineJobRunner;
+import org.springframework.batch.sample.domain.trade.CustomerUpdate;
+import org.springframework.batch.sample.domain.trade.InvalidCustomerLogger;
+
+/**
+ * @author Lucas Ward
+ *
+ */
+public class CommonsLoggingInvalidCustomerLogger implements
+ InvalidCustomerLogger {
+
+ protected static final Log logger = LogFactory.getLog(CommandLineJobRunner.class);
+
+ /* (non-Javadoc)
+ * @see org.springframework.batch.sample.domain.trade.InvalidCustomerLogger#log(org.springframework.batch.sample.domain.trade.CustomerUpdate)
+ */
+ public void log(CustomerUpdate customerUpdate) {
+ logger.error("invalid customer encountered: [ " + customerUpdate + "]");
+ }
+
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/JdbcCustomerDao.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/JdbcCustomerDao.java
new file mode 100644
index 000000000..3c9a45af6
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/domain/trade/internal/JdbcCustomerDao.java
@@ -0,0 +1,61 @@
+/**
+ *
+ */
+package org.springframework.batch.sample.domain.trade.internal;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.batch.sample.domain.trade.CustomerCredit;
+import org.springframework.batch.sample.domain.trade.CustomerDao;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.support.JdbcDaoSupport;
+
+/**
+ * @author Lucas Ward
+ *
+ */
+public class JdbcCustomerDao extends JdbcDaoSupport implements CustomerDao{
+
+ private static final String GET_CUSTOMER_BY_NAME = "SELECT ID, NAME, CREDIT from CUSTOMER where NAME = ?";
+ private static final String INSERT_CUSTOMER = "INSERT into CUSTOMER(NAME, CREDIT) values(?,?)";
+ private static final String UPDATE_CUSTOMER = "UPDATE CUSTOMER set CREDIT = ? where NAME = ?";
+
+ public CustomerCredit getCustomerByName(String name) {
+
+ @SuppressWarnings("unchecked")
+ List customers = (List) getJdbcTemplate().query(GET_CUSTOMER_BY_NAME, new Object[]{name},
+
+ new RowMapper(){
+
+ public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
+ CustomerCredit customer = new CustomerCredit();
+ customer.setName(rs.getString("NAME"));
+ customer.setId(rs.getInt("ID"));
+ customer.setCredit(rs.getBigDecimal("CREDIT"));
+ return customer;
+ }
+
+ });
+
+ if(customers.size() == 0){
+ return null;
+ }
+ else{
+ return customers.get(0);
+ }
+
+ }
+
+ public void insertCustomer(String name, BigDecimal credit) {
+
+ getJdbcTemplate().update(INSERT_CUSTOMER, new Object[]{name, credit});
+ }
+
+ public void updateCustomer(String name, BigDecimal credit) {
+ getJdbcTemplate().update(UPDATE_CUSTOMER, new Object[]{credit, name});
+ }
+
+}
diff --git a/spring-batch-samples/src/main/resources/data/customerFilterJob/input/customers.txt b/spring-batch-samples/src/main/resources/data/customerFilterJob/input/customers.txt
new file mode 100644
index 000000000..f50c5091e
--- /dev/null
+++ b/spring-batch-samples/src/main/resources/data/customerFilterJob/input/customers.txt
@@ -0,0 +1,6 @@
+SCust Credit
+Acustomer5 00032345
+Ucustomer3 00100500
+Acustomer6 00123456
+Dcustomer1 00000000
+F0000004
\ No newline at end of file
diff --git a/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml b/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml
new file mode 100644
index 000000000..8ef3362a4
--- /dev/null
+++ b/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/CustomerFilterJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/CustomerFilterJobFunctionalTests.java
new file mode 100644
index 000000000..bbe5edaa7
--- /dev/null
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/sample/CustomerFilterJobFunctionalTests.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2006-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.sample;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration()
+public class CustomerFilterJobFunctionalTests extends AbstractValidatingBatchLauncherTests {
+
+ private static final String GET_CUSTOMERS = "select NAME, CREDIT from CUSTOMER order by NAME";
+
+ private List customers;
+ private int activeRow = 0;
+
+ private SimpleJdbcTemplate simpleJdbcTemplate;
+ private Map credits = new HashMap();
+
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
+ }
+
+ @Before
+ public void onSetUp() throws Exception {
+ simpleJdbcTemplate.update("delete from TRADE");
+ List