package biz.c24.io.spring.batch.reader;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import biz.c24.io.api.ParserException;
import biz.c24.io.api.data.ComplexDataObject;
import biz.c24.io.api.data.ComplexDataType;
import biz.c24.io.api.data.DataType;
import biz.c24.io.api.data.Element;
import biz.c24.io.api.data.ValidationException;
import biz.c24.io.api.data.ValidationManager;
import biz.c24.io.api.presentation.ParseListener;
import biz.c24.io.api.presentation.Source;
import biz.c24.io.spring.batch.reader.source.SplittingReaderSource;
import biz.c24.io.spring.batch.reader.source.SplittingReader;
import biz.c24.io.spring.core.C24Model;
public class C24BatchItemReader implements ItemReader<ComplexDataObject> {
private Element element;
/**
* The source from which we'll read the data
*/
private SplittingReaderSource source;
private boolean validate = false;
private volatile Thread parsingThread = null;
/**
* Store this separately to make sure we report a job abort once only
*/
private volatile Throwable abortJobException = null;
private BlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(128);
private ThreadLocal<ValidationManager> validator = new ThreadLocal<ValidationManager>();
public void setModel(C24Model model) {
element = model.getRootElement();
DataType type = element.getType();
if(type instanceof ComplexDataType) {
((ComplexDataType) type).setProcessAsBatch(true);
}
}
/**
* Initialise our context
*
* @param stepExecution The step execution context
*/
@BeforeStep
public void setup(StepExecution stepExecution) {
source.initialise(stepExecution);
startParsing();
}
private void queueObject(ComplexDataObject obj) throws TimeoutException, InterruptedException {
if(!queue.offer(obj, 10, TimeUnit.SECONDS)) {
// TODO: Come up with a better way to propagating this up. The problem is we can't throw a checked type from the ParseListener callback
TimeoutException ex = new TimeoutException("Timed out waiting for parsed elements to be processed. Aborting.");
throw ex;
}
}
private void queueObject(ParserException obj) throws TimeoutException, InterruptedException {
if(!queue.offer(obj, 10, TimeUnit.SECONDS)) {
// TODO: Come up with a better way to propagating this up. The problem is we can't throw a checked type from the ParseListener callback
TimeoutException ex = new TimeoutException("Timed out waiting for parsed elements to be processed. Aborting.");
throw ex;
}
}
private void setParsingComplete() {
parsingThread = null;
}
/**
* Clean up and resources we're consuming
*/
@AfterStep
public void cleanup() {
source.close();
}
private void startParsing() {
parsingThread = new Thread(new IoParser());
parsingThread.start();
}
private Element getElement() {
return element;
}
public SplittingReaderSource getSource() {
return source;
}
public void setSource(SplittingReaderSource source) {
this.source = source;
}
private boolean stillParsing() {
return parsingThread != null;
}
@Override
public ComplexDataObject read() throws Exception, UnexpectedInputException,
ParseException, NonTransientResourceException {
ComplexDataObject cdo = null;
while(cdo == null && (!queue.isEmpty() || stillParsing())) {
try {
Object obj = queue.poll(1, TimeUnit.SECONDS);
if(obj != null) {
if(obj instanceof ParserException) {
throw new ParseException("Failed to parse file", (Throwable)obj);
} else if(obj instanceof ComplexDataObject) {
cdo = (ComplexDataObject)obj;
} else {
throw new ParseException("Unexpected type of object parsed: " + obj.getClass().getName());
}
}
} catch(InterruptedException ioEx) {
throw new ParseException("Interrupted while parsing", ioEx);
}
}
if(cdo == null && abortJobException != null) {
synchronized(this) {
if(abortJobException != null) {
Throwable ex = abortJobException;
abortJobException = null;
ParseException rethrow = ex instanceof ParseException? (ParseException)ex : new ParseException("Failure during parsing", ex);
throw rethrow;
}
}
} else if(cdo != null && validate) {
try {
ValidationManager mgr = validator.get();
if(mgr == null) {
mgr = new ValidationManager();
validator.set(mgr);
}
mgr.validateByException(cdo);
} catch(ValidationException vEx) {
throw new C24ValidationException("Failed to validate message: " + vEx.getLocalizedMessage() + " [" + source.getName() + "]", cdo, vEx);
}
}
return cdo;
}
public boolean isValidate() {
return validate;
}
public void setValidate(boolean validate) {
this.validate = validate;
}
private class IoParser implements ParseListener, Runnable {
public void run() {
try {
Source iOSource = getElement().getModel().source();
iOSource.setParseListener(this);
SplittingReader splitter = null;
while(abortJobException == null) {
try {
splitter = source.getReader();
if(splitter != null && !splitter.ready()) {
continue;
}
} catch (IOException ex) {
// Unhelpfully if the stream has been closed beneath our feet this is how we find out about it
// Even more unhelpfully, it appears as though the SAXParser does exactly that when it's finished parsing
break;
}
if(splitter == null) {
break;
}
iOSource.setReader(splitter.getReader());
iOSource.readObject(getElement());
}
} catch(Throwable ex) {
abortJobException = ex;
} finally {
setParsingComplete();
}
}
@Override
public void onStartBatch(Element element) throws ParserException {
// Add callback logic here
}
@Override
public void onEndBatch(Element element) throws ParserException {
// Add callback logic here
}
@Override
public Object onBatchEntryParsed(Object object) throws ParserException {
try {
if(object instanceof ComplexDataObject) {
queueObject((ComplexDataObject)object);
return null;
} else {
return object;
}
} catch(Exception ex) {
throw new ParserException(ex, ((ComplexDataObject)object).getName());
}
}
@Override
public void onBatchEntryFailed(Object object, ParserException failure)
throws ParserException {
try {
queueObject(failure);
// We can't read anything further from this reader
source.discard(source.getReader());
} catch(RuntimeException ex) {
// Rewrap any thrown exceptions so our caller can behave appropriately
throw new ParserException(ex, ((ComplexDataObject)object).getName());
} catch (TimeoutException ex) {
throw new ParserException(ex, ((ComplexDataObject)object).getName());
} catch (InterruptedException ex) {
throw new ParserException(ex, ((ComplexDataObject)object).getName());
} catch (IOException ex) {
throw new ParserException(ex, ((ComplexDataObject)object).getName());
}
}
@Override
public String generateFailedName(String original) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isAdditionalBatch(Element element) {
return false;
}
}
}