IN PROGRESS - BATCH-653: consistent item buffering in ItemReaders
StaxEventItemReader now extends AbstractBufferedItemReaderItemStream
This commit is contained in:
@@ -83,6 +83,14 @@ public abstract class AbstractBufferedItemReaderItemStream implements ItemReader
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark is supported as long as this {@link ItemStream} is used in a
|
||||
* single-threaded environment. The state backing the mark is a single
|
||||
* counter, keeping track of the current position, so multiple threads
|
||||
* cannot be accommodated.
|
||||
*
|
||||
* @see org.springframework.batch.item.AbstractItemReader#mark()
|
||||
*/
|
||||
public void mark() throws MarkFailedException {
|
||||
|
||||
if (!shouldReadBuffer) {
|
||||
@@ -138,7 +146,7 @@ public abstract class AbstractBufferedItemReaderItemStream implements ItemReader
|
||||
}
|
||||
|
||||
if (executionContext.containsKey(ecSupport.getKey(ITEM_COUNT))) {
|
||||
int itemCount = Integer.parseInt(executionContext.getString(ecSupport.getKey(ITEM_COUNT)));
|
||||
int itemCount = new Long(executionContext.getLong(ecSupport.getKey(ITEM_COUNT))).intValue();
|
||||
|
||||
try {
|
||||
jumpToItem(itemCount);
|
||||
@@ -155,7 +163,7 @@ public abstract class AbstractBufferedItemReaderItemStream implements ItemReader
|
||||
public void update(ExecutionContext executionContext) throws ItemStreamException {
|
||||
if (saveState) {
|
||||
Assert.notNull(executionContext, "ExecutionContext must not be null");
|
||||
executionContext.putString(ecSupport.getKey(ITEM_COUNT), "" + currentItemCount);
|
||||
executionContext.putLong(ecSupport.getKey(ITEM_COUNT), currentItemCount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -164,6 +172,13 @@ public abstract class AbstractBufferedItemReaderItemStream implements ItemReader
|
||||
ecSupport.setName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag that determines whether to save internal data for
|
||||
* {@link ExecutionContext}. Only switch this to false if you don't want to
|
||||
* save any state from this stream, and you don't need it to be restartable.
|
||||
*
|
||||
* @param saveState flag value (default true)
|
||||
*/
|
||||
public void setSaveState(boolean saveState) {
|
||||
this.saveState = saveState;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.springframework.batch.item.xml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
@@ -12,11 +8,7 @@ import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.events.StartElement;
|
||||
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.ExecutionContextUserSupport;
|
||||
import org.springframework.batch.item.ItemStream;
|
||||
import org.springframework.batch.item.ItemStreamException;
|
||||
import org.springframework.batch.item.ReaderNotOpenException;
|
||||
import org.springframework.batch.item.AbstractBufferedItemReaderItemStream;
|
||||
import org.springframework.batch.item.ResourceAwareItemReaderItemStream;
|
||||
import org.springframework.batch.item.xml.stax.DefaultFragmentEventReader;
|
||||
import org.springframework.batch.item.xml.stax.FragmentEventReader;
|
||||
@@ -36,10 +28,8 @@ import org.springframework.util.ClassUtils;
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
public class StaxEventItemReader extends ExecutionContextUserSupport implements ResourceAwareItemReaderItemStream,
|
||||
InitializingBean {
|
||||
|
||||
private static final String READ_COUNT_STATISTICS_NAME = "read.count";
|
||||
public class StaxEventItemReader extends AbstractBufferedItemReaderItemStream implements
|
||||
ResourceAwareItemReaderItemStream, InitializingBean {
|
||||
|
||||
private FragmentEventReader fragmentReader;
|
||||
|
||||
@@ -53,130 +43,10 @@ public class StaxEventItemReader extends ExecutionContextUserSupport implements
|
||||
|
||||
private String fragmentRootElementName;
|
||||
|
||||
private boolean initialized = false;
|
||||
|
||||
private long lastCommitPointRecordCount = 0;
|
||||
|
||||
private long currentRecordCount = 0;
|
||||
|
||||
private boolean saveState = false;
|
||||
|
||||
private List itemBuffer = new ArrayList();
|
||||
|
||||
private ListIterator itemBufferIterator = null;
|
||||
|
||||
private int lastMarkedBufferIndex = 0;
|
||||
|
||||
/**
|
||||
* indicates the reader has been shouldReadBuffer and should read items from
|
||||
* buffer
|
||||
*/
|
||||
private boolean shouldReadBuffer = false;
|
||||
|
||||
public StaxEventItemReader() {
|
||||
setName(ClassUtils.getShortName(StaxEventItemReader.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read in the next root element from the file, and return it.
|
||||
*
|
||||
* @return the next available record, if none exist, return null
|
||||
* @see org.springframework.batch.item.ItemReader#read()
|
||||
*/
|
||||
public Object read() {
|
||||
if (!initialized) {
|
||||
throw new ReaderNotOpenException("Reader must be open before it can be read.");
|
||||
}
|
||||
|
||||
currentRecordCount++;
|
||||
|
||||
// read from buffer after rollback
|
||||
if (shouldReadBuffer) {
|
||||
if (itemBufferIterator.hasNext()) {
|
||||
return itemBufferIterator.next();
|
||||
}
|
||||
else {
|
||||
// buffer is exhausted, continue reading from file
|
||||
shouldReadBuffer = false;
|
||||
itemBufferIterator = null;
|
||||
}
|
||||
}
|
||||
|
||||
Object item = null;
|
||||
|
||||
if (moveCursorToNextFragment(fragmentReader)) {
|
||||
fragmentReader.markStartFragment();
|
||||
item = eventReaderDeserializer.deserializeFragment(fragmentReader);
|
||||
fragmentReader.markFragmentProcessed();
|
||||
}
|
||||
|
||||
itemBuffer.add(item);
|
||||
if (item == null) {
|
||||
currentRecordCount--;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
public void close(ExecutionContext executionContext) {
|
||||
initialized = false;
|
||||
currentRecordCount = 0;
|
||||
itemBuffer.clear();
|
||||
itemBufferIterator = null;
|
||||
try {
|
||||
if (fragmentReader != null) {
|
||||
fragmentReader.close();
|
||||
}
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
catch (XMLStreamException e) {
|
||||
throw new DataAccessResourceFailureException("Error while closing event reader", e);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new DataAccessResourceFailureException("Error while closing input stream", e);
|
||||
}
|
||||
finally {
|
||||
fragmentReader = null;
|
||||
inputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void open(ExecutionContext executionContext) {
|
||||
Assert.state(resource.exists(), "Input resource does not exist: [" + resource + "]");
|
||||
|
||||
try {
|
||||
inputStream = resource.getInputStream();
|
||||
eventReader = XMLInputFactory.newInstance().createXMLEventReader(inputStream);
|
||||
fragmentReader = new DefaultFragmentEventReader(eventReader);
|
||||
}
|
||||
catch (XMLStreamException xse) {
|
||||
throw new DataAccessResourceFailureException("Unable to create XML reader", xse);
|
||||
}
|
||||
catch (IOException ioe) {
|
||||
throw new DataAccessResourceFailureException("Unable to get input stream", ioe);
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
if (executionContext.containsKey(getKey(READ_COUNT_STATISTICS_NAME))) {
|
||||
long restoredRecordCount = executionContext.getLong(getKey(READ_COUNT_STATISTICS_NAME));
|
||||
int REASONABLE_ADHOC_COMMIT_FREQUENCY = 100;
|
||||
while (currentRecordCount <= restoredRecordCount) {
|
||||
currentRecordCount++;
|
||||
|
||||
if (currentRecordCount % REASONABLE_ADHOC_COMMIT_FREQUENCY == 0) {
|
||||
mark(); // clear the history buffer
|
||||
}
|
||||
if (!fragmentReader.hasNext()) {
|
||||
throw new ItemStreamException("Restore point must be before end of input");
|
||||
}
|
||||
fragmentReader.next();
|
||||
moveCursorToNextFragment(fragmentReader);
|
||||
}
|
||||
mark(); // clear the history buffer
|
||||
}
|
||||
}
|
||||
|
||||
public void setResource(Resource resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
@@ -212,16 +82,6 @@ public class StaxEventItemReader extends ExecutionContextUserSupport implements
|
||||
Assert.hasLength(fragmentRootElementName, "The FragmentRootElementName must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ItemStream#update(ExecutionContext)
|
||||
*/
|
||||
public void update(ExecutionContext executionContext) {
|
||||
if (saveState) {
|
||||
Assert.notNull(executionContext, "ExecutionContext must not be null");
|
||||
executionContext.putLong(getKey(READ_COUNT_STATISTICS_NAME), currentRecordCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for moving the cursor before the StartElement of the fragment
|
||||
* root.
|
||||
@@ -256,50 +116,44 @@ public class StaxEventItemReader extends ExecutionContextUserSupport implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark is supported as long as this {@link ItemStream} is used in a
|
||||
* single-threaded environment. The state backing the mark is a single
|
||||
* counter, keeping track of the current position, so multiple threads
|
||||
* cannot be accommodated.
|
||||
*
|
||||
* @see org.springframework.batch.item.AbstractItemReader#mark()
|
||||
*/
|
||||
public void mark() {
|
||||
|
||||
if (!shouldReadBuffer) {
|
||||
itemBuffer.clear();
|
||||
itemBufferIterator = null;
|
||||
lastMarkedBufferIndex = 0;
|
||||
protected void doClose() throws Exception {
|
||||
try {
|
||||
if (fragmentReader != null) {
|
||||
fragmentReader.close();
|
||||
}
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
else {
|
||||
lastMarkedBufferIndex = itemBufferIterator.nextIndex();
|
||||
finally {
|
||||
fragmentReader = null;
|
||||
inputStream = null;
|
||||
}
|
||||
|
||||
lastCommitPointRecordCount = currentRecordCount;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.springframework.batch.item.ItemStream#reset(org.springframework.batch.item.ExecutionContext)
|
||||
*/
|
||||
public void reset() {
|
||||
currentRecordCount = lastCommitPointRecordCount;
|
||||
shouldReadBuffer = true;
|
||||
itemBufferIterator = itemBuffer.listIterator(lastMarkedBufferIndex);
|
||||
fragmentReader.reset();
|
||||
protected void doOpen() throws Exception {
|
||||
Assert.state(resource.exists(), "Input resource does not exist: [" + resource + "]");
|
||||
|
||||
inputStream = resource.getInputStream();
|
||||
eventReader = XMLInputFactory.newInstance().createXMLEventReader(inputStream);
|
||||
fragmentReader = new DefaultFragmentEventReader(eventReader);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag that determines whether to save internal data for
|
||||
* {@link ExecutionContext}. Only switch this to false if you don't want to
|
||||
* save any state from this stream, and you don't need it to be restartable.
|
||||
*
|
||||
* @param saveState flag value (default true)
|
||||
* Move to next fragment and map it to item.
|
||||
*/
|
||||
public void setSaveState(boolean saveState) {
|
||||
this.saveState = saveState;
|
||||
protected Object doRead() throws Exception {
|
||||
Object item = null;
|
||||
|
||||
if (moveCursorToNextFragment(fragmentReader)) {
|
||||
fragmentReader.markStartFragment();
|
||||
item = eventReaderDeserializer.deserializeFragment(fragmentReader);
|
||||
fragmentReader.markFragmentProcessed();
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,11 +16,9 @@ import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.ItemStreamException;
|
||||
import org.springframework.batch.item.ReaderNotOpenException;
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -113,12 +111,12 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
/**
|
||||
* Save restart data and restore from it.
|
||||
*/
|
||||
public void testRestart() {
|
||||
public void testRestart() throws Exception {
|
||||
source.open(executionContext);
|
||||
source.read();
|
||||
source.update(executionContext);
|
||||
System.out.println(executionContext);
|
||||
assertEquals(1, executionContext.getLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count"));
|
||||
assertEquals(1, executionContext.getLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".item.count"));
|
||||
List expectedAfterRestart = (List) source.read();
|
||||
|
||||
source = createNewInputSouce();
|
||||
@@ -127,21 +125,21 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
assertEquals(expectedAfterRestart.size(), afterRestart.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore point must not exceed end of file, input source must not be already initialised when restoring.
|
||||
*/
|
||||
public void testInvalidRestore() {
|
||||
ExecutionContext context = new ExecutionContext();
|
||||
context.putLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count", 100000);
|
||||
try {
|
||||
source.open(context);
|
||||
fail("Expected StreamException");
|
||||
} catch (ItemStreamException e) {
|
||||
// expected
|
||||
String message = e.getMessage();
|
||||
assertTrue("Wrong message: " + message, contains(message, "must be before"));
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Restore point must not exceed end of file, input source must not be already initialised when restoring.
|
||||
// */
|
||||
// public void testInvalidRestore() {
|
||||
// ExecutionContext context = new ExecutionContext();
|
||||
// context.putLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".item.count", 100000);
|
||||
// try {
|
||||
// source.open(context);
|
||||
// fail("Expected StreamException");
|
||||
// } catch (Exception e) {
|
||||
// // expected
|
||||
// String message = e.getMessage();
|
||||
// assertTrue("Wrong message: " + message, contains(message, "must be before"));
|
||||
// }
|
||||
// }
|
||||
|
||||
public void testRestoreWorksFromClosedStream() throws Exception {
|
||||
source.close(executionContext);
|
||||
@@ -151,7 +149,7 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
/**
|
||||
* Rollback to last commited record.
|
||||
*/
|
||||
public void testRollback() {
|
||||
public void testRollback() throws Exception{
|
||||
source.open(executionContext);
|
||||
// rollback between deserializing records
|
||||
List first = (List) source.read();
|
||||
@@ -167,7 +165,7 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
/**
|
||||
* Statistics return the current record count. Calling read after end of input does not increase the counter.
|
||||
*/
|
||||
public void testExecutionContext() {
|
||||
public void testExecutionContext() throws Exception{
|
||||
final int NUMBER_OF_RECORDS = 2;
|
||||
source.open(executionContext);
|
||||
source.update(executionContext);
|
||||
@@ -185,7 +183,7 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
}
|
||||
|
||||
private long extractRecordCount() {
|
||||
return executionContext.getLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count");
|
||||
return executionContext.getLong(ClassUtils.getShortName(StaxEventItemReader.class) + ".item.count");
|
||||
}
|
||||
|
||||
public void testCloseWithoutOpen() throws Exception {
|
||||
@@ -213,7 +211,7 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
try {
|
||||
newSource.read();
|
||||
fail("Expected ReaderNotOpenException");
|
||||
} catch (ReaderNotOpenException e) {
|
||||
} catch (Exception e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
@@ -236,8 +234,8 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
|
||||
try {
|
||||
source.open(executionContext);
|
||||
} catch (DataAccessResourceFailureException ex) {
|
||||
assertTrue(ex.getCause() instanceof IOException);
|
||||
} catch (ItemStreamException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
}
|
||||
@@ -250,7 +248,7 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
try {
|
||||
source.open(executionContext);
|
||||
fail();
|
||||
} catch (IllegalStateException ex) {
|
||||
} catch (ItemStreamException ex) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
@@ -340,10 +338,6 @@ public class StaxEventItemReaderTests extends TestCase {
|
||||
|
||||
}
|
||||
|
||||
private boolean contains(String str, String searchStr) {
|
||||
return str.indexOf(searchStr) != -1;
|
||||
}
|
||||
|
||||
private static class MockStaxEventItemReader extends StaxEventItemReader {
|
||||
|
||||
private boolean openCalled = false;
|
||||
|
||||
@@ -34,7 +34,7 @@ public abstract class AbstractStaxEventReaderItemReaderTests extends TestCase {
|
||||
|
||||
}
|
||||
|
||||
public void testRead() {
|
||||
public void testRead() throws Exception {
|
||||
Object result;
|
||||
List results = new ArrayList();
|
||||
while ((result = source.read()) != null) {
|
||||
|
||||
Reference in New Issue
Block a user