/*
* Copyright 2006-2013 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.core.step.item;
import java.util.List;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.core.listener.MulticasterBatchListener;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Simple implementation of the {@link ChunkProcessor} interface that handles
* basic item writing and processing. Any exceptions encountered will be
* rethrown.
*
* @see ChunkOrientedTasklet
*/
public class SimpleChunkProcessor<I, O> implements ChunkProcessor<I>, InitializingBean {
private ItemProcessor<? super I, ? extends O> itemProcessor;
private ItemWriter<? super O> itemWriter;
private final MulticasterBatchListener<I, O> listener = new MulticasterBatchListener<I, O>();
/**
* Default constructor for ease of configuration (both itemWriter and
* itemProcessor are mandatory).
*/
@SuppressWarnings("unused")
private SimpleChunkProcessor() {
this(null, null);
}
public SimpleChunkProcessor(ItemProcessor<? super I, ? extends O> itemProcessor, ItemWriter<? super O> itemWriter) {
this.itemProcessor = itemProcessor;
this.itemWriter = itemWriter;
}
/**
* @param itemProcessor the {@link ItemProcessor} to set
*/
public void setItemProcessor(ItemProcessor<? super I, ? extends O> itemProcessor) {
this.itemProcessor = itemProcessor;
}
/**
* @param itemWriter the {@link ItemWriter} to set
*/
public void setItemWriter(ItemWriter<? super O> itemWriter) {
this.itemWriter = itemWriter;
}
/**
* Check mandatory properties.
*
* @see InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(itemWriter, "ItemWriter must be set");
Assert.notNull(itemProcessor, "ItemProcessor must be set");
}
/**
* Register some {@link StepListener}s with the handler. Each will get the
* callbacks in the order specified at the correct stage.
*
* @param listeners
*/
public void setListeners(List<? extends StepListener> listeners) {
for (StepListener listener : listeners) {
registerListener(listener);
}
}
/**
* Register a listener for callbacks at the appropriate stages in a process.
*
* @param listener a {@link StepListener}
*/
public void registerListener(StepListener listener) {
this.listener.register(listener);
}
/**
* @return the listener
*/
protected MulticasterBatchListener<I, O> getListener() {
return listener;
}
/**
* @param item the input item
* @return the result of the processing
* @throws Exception
*/
protected final O doProcess(I item) throws Exception {
if (itemProcessor == null) {
@SuppressWarnings("unchecked")
O result = (O) item;
return result;
}
try {
listener.beforeProcess(item);
O result = itemProcessor.process(item);
listener.afterProcess(item, result);
return result;
}
catch (Exception e) {
listener.onProcessError(item, e);
throw e;
}
}
/**
* Surrounds the actual write call with listener callbacks.
*
* @param items
* @throws Exception
*/
protected final void doWrite(List<O> items) throws Exception {
if (itemWriter == null) {
return;
}
try {
listener.beforeWrite(items);
writeItems(items);
doAfterWrite(items);
}
catch (Exception e) {
doOnWriteError(e, items);
throw e;
}
}
/**
* Call the listener's after write method.
*
* @param items
*/
protected final void doAfterWrite(List<O> items) {
listener.afterWrite(items);
}
protected final void doOnWriteError(Exception e, List<O> items) {
listener.onWriteError(e, items);
}
protected void writeItems(List<O> items) throws Exception {
if (itemWriter != null) {
itemWriter.write(items);
}
}
@Override
public final void process(StepContribution contribution, Chunk<I> inputs) throws Exception {
// Allow temporary state to be stored in the user data field
initializeUserData(inputs);
// If there is no input we don't have to do anything more
if (isComplete(inputs)) {
return;
}
// Make the transformation, calling remove() on the inputs iterator if
// any items are filtered. Might throw exception and cause rollback.
Chunk<O> outputs = transform(contribution, inputs);
// Adjust the filter count based on available data
contribution.incrementFilterCount(getFilterCount(inputs, outputs));
// Adjust the outputs if necessary for housekeeping purposes, and then
// write them out...
write(contribution, inputs, getAdjustedOutputs(inputs, outputs));
}
/**
* Extension point for subclasses to allow them to memorise the contents of
* the inputs, in case they are needed for accounting purposes later. The
* default implementation sets up some user data to remember the original
* size of the inputs. If this method is overridden then some or all of
* {@link #isComplete(Chunk)}, {@link #getFilterCount(Chunk, Chunk)} and
* {@link #getAdjustedOutputs(Chunk, Chunk)} might also need to be, to
* ensure that the user data is handled consistently.
*
* @param inputs the inputs for the process
*/
protected void initializeUserData(Chunk<I> inputs) {
inputs.setUserData(inputs.size());
}
/**
* Extension point for subclasses to calculate the filter count. Defaults to
* the difference between input size and output size.
*
* @param inputs the inputs after transformation
* @param outputs the outputs after transformation
*
* @return the difference in sizes
*
* @see #initializeUserData(Chunk)
*/
protected int getFilterCount(Chunk<I> inputs, Chunk<O> outputs) {
return (Integer) inputs.getUserData() - outputs.size();
}
/**
* Extension point for subclasses that want to store additional data in the
* inputs. Default just checks if inputs are empty.
*
* @param inputs the input chunk
* @return true if it is empty
*
* @see #initializeUserData(Chunk)
*/
protected boolean isComplete(Chunk<I> inputs) {
return inputs.isEmpty();
}
/**
* Extension point for subclasses that want to adjust the outputs based on
* additional saved data in the inputs. Default implementation just returns
* the outputs unchanged.
*
* @param inputs the inputs for the transformation
* @param outputs the result of the transformation
* @return the outputs unchanged
*
* @see #initializeUserData(Chunk)
*/
protected Chunk<O> getAdjustedOutputs(Chunk<I> inputs, Chunk<O> outputs) {
return outputs;
}
/**
* Simple implementation delegates to the {@link #doWrite(List)} method and
* increments the write count in the contribution. Subclasses can handle
* more complicated scenarios, e.g.with fault tolerance. If output items are
* skipped they should be removed from the inputs as well.
*
* @param contribution the current step contribution
* @param inputs the inputs that gave rise to the outputs
* @param outputs the outputs to write
* @throws Exception if there is a problem
*/
protected void write(StepContribution contribution, Chunk<I> inputs, Chunk<O> outputs) throws Exception {
try {
doWrite(outputs.getItems());
}
catch (Exception e) {
/*
* For a simple chunk processor (no fault tolerance) we are done
* here, so prevent any more processing of these inputs.
*/
inputs.clear();
throw e;
}
contribution.incrementWriteCount(outputs.size());
}
protected Chunk<O> transform(StepContribution contribution, Chunk<I> inputs) throws Exception {
Chunk<O> outputs = new Chunk<O>();
for (Chunk<I>.ChunkIterator iterator = inputs.iterator(); iterator.hasNext();) {
final I item = iterator.next();
O output;
try {
output = doProcess(item);
}
catch (Exception e) {
/*
* For a simple chunk processor (no fault tolerance) we are done
* here, so prevent any more processing of these inputs.
*/
inputs.clear();
throw e;
}
if (output != null) {
outputs.add(output);
}
else {
iterator.remove();
}
}
return outputs;
}
}