diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java
new file mode 100644
index 000000000..4dee0dc7e
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2006-2008 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.item.database;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.batch.item.ItemWriter;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.dao.InvalidDataAccessResourceUsageException;
+import org.springframework.orm.ibatis.SqlMapClientCallback;
+import org.springframework.orm.ibatis.SqlMapClientOperations;
+import org.springframework.orm.ibatis.SqlMapClientTemplate;
+import org.springframework.util.Assert;
+
+import com.ibatis.sqlmap.client.SqlMapClient;
+import com.ibatis.sqlmap.client.SqlMapExecutor;
+import com.ibatis.sqlmap.engine.execution.BatchException;
+import com.ibatis.sqlmap.engine.execution.BatchResult;
+
+/**
+ * {@link ItemWriter} that uses the batching features from
+ * {@link SqlMapClientTemplate} to execute a batch of statements for all items
+ * provided.
+ *
+ * The user must provide an iBATIS statement id that points to the SQL statement defined
+ * in the iBATIS SqlMap configuration.
+ *
+ * It is expected that {@link #write(List)} is called inside a transaction.
+ *
+ * The writer is thread safe after its properties are set (normal singleton
+ * behavior), so it can be used to write in multiple concurrent transactions.
+ *
+ * @author Thomas Risberg
+ * @since 2.0
+ */
+public class IbatisBatchItemWriter implements ItemWriter, InitializingBean {
+
+ protected static final Log logger = LogFactory.getLog(IbatisBatchItemWriter.class);
+
+ private SqlMapClientTemplate sqlMapClientTemplate;
+
+ private String statementId;
+
+ private boolean assertUpdates = true;
+
+ /**
+ * Public setter for the flag that determines whether an assertion is made
+ * that all items cause at least one row to be updated.
+ *
+ * @param assertUpdates the flag to set. Defaults to true;
+ */
+ public void setAssertUpdates(boolean assertUpdates) {
+ this.assertUpdates = assertUpdates;
+ }
+
+ /**
+ * Public setter for {@link SqlMapClient} for injection purposes.
+ *
+ * @param sqlMapClient the SqlMapClient
+ */
+ public void setSqlMapClient(SqlMapClient sqlMapClient) {
+ if (sqlMapClientTemplate == null) {
+ this.sqlMapClientTemplate = new SqlMapClientTemplate(sqlMapClient);
+ }
+ }
+
+ /**
+ * Public setter for the {@link SqlMapClientOperations}.
+ *
+ * @param sqlMapClientTemplate the SqlMapClientOperations
+ */
+ public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) {
+ this.sqlMapClientTemplate = sqlMapClientTemplate;
+ }
+
+ /**
+ * Public setter for the statement id identifying the statement in the SqlMap
+ * configuration file.
+ *
+ * @param statementId the id for the statement
+ */
+ public void setStatementId(String statementId) {
+ this.statementId = statementId;
+ }
+
+ /**
+ * Check mandatory properties - there must be an SqlMapCLient and a statementId.
+ */
+ public void afterPropertiesSet() throws Exception {
+ Assert.notNull(sqlMapClientTemplate, "A SqlMapClient or a SqlMapClientTemplate is required.");
+ Assert.notNull(statementId, "A statementId is required.");
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.batch.item.ItemWriter#write(java.util.List)
+ */
+ public void write(final List extends T> items) throws Exception {
+
+ if (!items.isEmpty()) {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing batch with " + items.size() + " items.");
+ }
+
+ @SuppressWarnings("unchecked")
+ List results = (List) sqlMapClientTemplate.execute(
+ new SqlMapClientCallback() {
+ public Object doInSqlMapClient(SqlMapExecutor executor)
+ throws SQLException {
+ executor.startBatch();
+ for (T item : items) {
+ executor.update(statementId, item);
+ }
+ try {
+ return executor.executeBatchDetailed();
+ } catch (BatchException e) {
+ throw e.getBatchUpdateException();
+ }
+ }
+ });
+
+ if (assertUpdates) {
+ if (results.size() != 1) {
+ throw new InvalidDataAccessResourceUsageException("Batch execution returned invalid results. " +
+ "Expected 1 but number of BatchResult objects returned was " + results.size());
+ }
+
+ int[] updateCounts = results.get(0).getUpdateCounts();
+
+ for (int i = 0; i < updateCounts.length; i++) {
+ int value = updateCounts[i];
+ if (value == 0) {
+ throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length
+ + " did not update any rows: [" + items.get(i) + "]", 1);
+ }
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java
new file mode 100644
index 000000000..c2411fadf
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2006-2008 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.item.database;
+
+import static org.junit.Assert.*;
+import static org.easymock.EasyMock.*;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.batch.repeat.support.RepeatSynchronizationManager;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.orm.ibatis.SqlMapClientTemplate;
+
+import com.ibatis.sqlmap.client.SqlMapClient;
+import com.ibatis.sqlmap.client.SqlMapSession;
+import com.ibatis.sqlmap.engine.execution.BatchResult;
+
+/**
+ * @author Thomas Risberg
+ */
+public class IbatisBatchItemWriterTests {
+
+ private IbatisBatchItemWriter writer = new IbatisBatchItemWriter();
+
+ private DataSource ds;
+
+ private SqlMapClientTemplate smct;
+
+ private SqlMapClient smc;
+
+ private String statementId = "updateFoo";
+
+ private class Foo {
+ private Long id;
+ private String bar;
+
+ public Foo(String bar) {
+ this.id = 1L;
+ this.bar = bar;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getBar() {
+ return bar;
+ }
+
+ public void setBar(String bar) {
+ this.bar = bar;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Foo)) {
+ return false;
+ }
+ Foo compare = (Foo) obj;
+ if (this.bar.equals(compare.getBar())) {
+ return true;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return bar.hashCode();
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Before
+ public void setUp() throws Exception {
+ smc = createMock(SqlMapClient.class);
+ ds = createNiceMock(DataSource.class);
+ smct = new SqlMapClientTemplate(ds, smc);
+ writer.setStatementId(statementId);
+ writer.setSqlMapClientTemplate(smct);
+ writer.afterPropertiesSet();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see junit.framework.TestCase#tearDown()
+ */
+ @After
+ public void tearDown() throws Exception {
+ RepeatSynchronizationManager.clear();
+ }
+
+ /**
+ * Test method for
+ * {@link org.springframework.batch.item.database.JdbcBatchItemWriter#afterPropertiesSet()}
+ * .
+ * @throws Exception
+ */
+ @Test
+ public void testAfterPropertiesSet() throws Exception {
+ writer = new IbatisBatchItemWriter();
+ try {
+ writer.afterPropertiesSet();
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ String message = e.getMessage();
+ assertTrue("Message does not contain 'SqlMapClient'.", message.indexOf("SqlMapClient") >= 0);
+ }
+ writer.setSqlMapClientTemplate(smct);
+ try {
+ writer.afterPropertiesSet();
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ String message = e.getMessage();
+ assertTrue("Message does not contain 'statementId'.", message.indexOf("statementId") >= 0);
+ }
+ writer.setStatementId("statementId");
+ writer.afterPropertiesSet();
+ }
+
+ @Test
+ public void testWriteAndFlush() throws Exception {
+ SqlMapSession sms = createMock(SqlMapSession.class);
+ expect(smc.openSession()).andReturn(sms);
+ sms.close();
+ expect(sms.getCurrentConnection()).andReturn(null);
+ sms.setUserConnection(null);
+ sms.startBatch();
+ expect(sms.update("updateFoo", new Foo("bar"))).andReturn(-2);
+ List results = Collections.singletonList(new BatchResult("updateFoo", "update foo"));
+ results.get(0).setUpdateCounts(new int[] {1});
+ expect(sms.executeBatchDetailed()).andReturn(results);
+ replay(sms);
+ replay(smc);
+ writer.write(Collections.singletonList(new Foo("bar")));
+ verify(sms);
+ verify(smc);
+ }
+
+ @Test
+ public void testWriteAndFlushWithEmptyUpdate() throws Exception {
+ SqlMapSession sms = createMock(SqlMapSession.class);
+ expect(smc.openSession()).andReturn(sms);
+ sms.close();
+ expect(sms.getCurrentConnection()).andReturn(null);
+ sms.setUserConnection(null);
+ sms.startBatch();
+ expect(sms.update("updateFoo", new Foo("bar"))).andReturn(1);
+ List results = Collections.singletonList(new BatchResult("updateFoo", "update foo"));
+ results.get(0).setUpdateCounts(new int[] {0});
+ expect(sms.executeBatchDetailed()).andReturn(results);
+ replay(sms);
+ replay(smc);
+ try {
+ writer.write(Collections.singletonList(new Foo("bar")));
+ fail("Expected EmptyResultDataAccessException");
+ }
+ catch (EmptyResultDataAccessException e) {
+ // expected
+ String message = e.getMessage();
+ assertTrue("Wrong message: " + message, message.indexOf("did not update") >= 0);
+ }
+ verify(sms);
+ verify(smc);
+ }
+
+ @Test
+ public void testWriteAndFlushWithFailure() throws Exception {
+ final RuntimeException ex = new RuntimeException("ERROR");
+ SqlMapSession sms = createMock(SqlMapSession.class);
+ expect(smc.openSession()).andReturn(sms);
+ sms.close();
+ expect(sms.getCurrentConnection()).andReturn(null);
+ sms.setUserConnection(null);
+ sms.startBatch();
+ expect(sms.update("updateFoo", new Foo("bar"))).andThrow(ex);
+ replay(sms);
+ replay(smc);
+ try {
+ writer.write(Collections.singletonList(new Foo("bar")));
+ fail("Expected RuntimeException");
+ }
+ catch (RuntimeException e) {
+ assertEquals("ERROR", e.getMessage());
+ }
+ verify(sms);
+ verify(smc);
+ }
+
+}