diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/ResourceLineReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/ResourceLineReader.java index d4b68954b..482593333 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/ResourceLineReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/ResourceLineReader.java @@ -9,9 +9,11 @@ import org.apache.commons.logging.LogFactory; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ReaderNotOpenException; import org.springframework.batch.item.UnexpectedInputException; +import org.springframework.batch.item.file.mapping.LineMapper; import org.springframework.batch.item.file.separator.RecordSeparatorPolicy; import org.springframework.batch.item.file.separator.SimpleRecordSeparatorPolicy; import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -19,12 +21,13 @@ import org.springframework.util.ClassUtils; /** * Restartable {@link ItemReader} that reads lines from input * {@link #setResource(Resource)}. Line is defined by the - * {@link #setRecordSeparatorPolicy(RecordSeparatorPolicy)}. + * {@link #setRecordSeparatorPolicy(RecordSeparatorPolicy)} and mapped to item + * using {@link #setLineMapper(LineMapper)}. * * @author Robert Kasanicky */ -public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader implements - ResourceAwareItemReaderItemStream { +public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader implements + ResourceAwareItemReaderItemStream, InitializingBean { private static final Log logger = LogFactory.getLog(ResourceLineReader.class); @@ -44,11 +47,21 @@ public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader private boolean noInput = false; private String encoding = DEFAULT_CHARSET; - + + private LineMapper lineMapper; + public ResourceLineReader() { setName(ClassUtils.getShortName(FlatFileItemReader.class)); } + /** + * Setter for line mapper. This property is required to be set. + * @param lineMapper maps line to item + */ + public void setLineMapper(LineMapper lineMapper) { + this.lineMapper = lineMapper; + } + /** * Setter for the encoding for this input source. Default value is * {@link #DEFAULT_CHARSET}. @@ -95,7 +108,7 @@ public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader * multiple lines in file). */ @Override - protected String doRead() { + protected T doRead() throws Exception { if (noInput) { return null; } @@ -106,7 +119,13 @@ public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader record = recordSeparatorPolicy.preProcess(record) + (line = readLine()); } } - return recordSeparatorPolicy.postProcess(record); + String logicalLine = recordSeparatorPolicy.postProcess(record); + if (logicalLine == null) { + return null; + } + else { + return lineMapper.mapLine(logicalLine, lineCount); + } } /** @@ -173,4 +192,8 @@ public class ResourceLineReader extends AbstractItemCountingItemStreamItemReader } + public void afterPropertiesSet() throws Exception { + Assert.notNull(lineMapper, "LineMapper is required"); + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/DefaultLineMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/DefaultLineMapper.java new file mode 100644 index 000000000..2d94dd360 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/DefaultLineMapper.java @@ -0,0 +1,31 @@ +package org.springframework.batch.item.file.mapping; + +import org.springframework.batch.item.file.transform.LineTokenizer; + +/** + * Two-phase {@link LineMapper} implementation consisting of tokenization of the + * line into {@link FieldSet} followed by mapping to item. + * + * @author Robert Kasanicky + * + * @param type of the item + */ +public class DefaultLineMapper implements LineMapper { + + private LineTokenizer tokenizer; + + private FieldSetMapper fieldSetMapper; + + public T mapLine(String line, int lineNumber) throws Exception { + return fieldSetMapper.process(tokenizer.process(line)); + } + + public void setTokenizer(LineTokenizer tokenizer) { + this.tokenizer = tokenizer; + } + + public void setFieldSetMapper(FieldSetMapper fieldSetMapper) { + this.fieldSetMapper = fieldSetMapper; + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/LineMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/LineMapper.java new file mode 100644 index 000000000..40e472df1 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/LineMapper.java @@ -0,0 +1,13 @@ +package org.springframework.batch.item.file.mapping; + +/** + * Interface for mapping lines (strings) to domain objects. + * + * @author Robert Kasanicky + * + * @param type of the domain object + */ +public interface LineMapper { + + T mapLine(String line, int lineNumber) throws Exception; +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PassThroughLineMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PassThroughLineMapper.java new file mode 100644 index 000000000..03b6e355e --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PassThroughLineMapper.java @@ -0,0 +1,9 @@ +package org.springframework.batch.item.file.mapping; + +public class PassThroughLineMapper implements LineMapper{ + + public String mapLine(String line, int lineNumber) throws Exception { + return line; + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/ResourceLineReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/ResourceLineReaderTests.java index e07b7dea2..6c25d5f25 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/ResourceLineReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/ResourceLineReaderTests.java @@ -1,169 +1,50 @@ package org.springframework.batch.item.file; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import org.junit.Before; -import org.junit.Test; +import org.springframework.batch.item.CommonItemStreamItemReaderTests; import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.file.separator.RecordSeparatorPolicy; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.file.mapping.LineMapper; +import org.springframework.batch.item.sample.Foo; import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; /** * Tests for {@link ResourceLineReader}. */ -public class ResourceLineReaderTests { +public class ResourceLineReaderTests extends CommonItemStreamItemReaderTests{ - private ResourceLineReader tested; - - private static final Resource resource = new ByteArrayResource("line1\nline2\nline3\nline4\nline5".getBytes()); - - private ExecutionContext executionContext = new ExecutionContext(); - - private static ResourceLineReader getItemReader() { - ResourceLineReader result = new ResourceLineReader(); - result.setResource(resource); - return result; - } - - @Before - public void setUp() throws Exception { - tested = getItemReader(); - tested.open(executionContext); - } - - /** - * Regular scenario - read the input and eventually return null. - */ - @Test - public void testRead() throws Exception { - - assertEquals("line1", tested.read()); - assertEquals("line2", tested.read()); - assertEquals("line3", tested.read()); - assertEquals("line4", tested.read()); - assertEquals("line5", tested.read()); - - assertNull(tested.read()); - } - - /** - * No input should be handled gracefully - null is returned on first - * read. - */ - @Test - public void testNoInput() throws Exception { - tested = getItemReader(); - tested.setResource(new DescriptiveResource("doesn't exist") { - - @Override - public boolean exists() { - return false; - } - - }); - tested.open(executionContext); - assertNull(tested.read()); - } - - /** - * Restart scenario - read items, update execution context, create new - * reader and restore from restart data - the new input source should - * continue where the old one finished. - */ - @Test - public void testRestart() throws Exception { - - tested.update(executionContext); - - assertEquals("line1", tested.read()); - assertEquals("line2", tested.read()); - - tested.update(executionContext); - - // create new input source - tested = getItemReader(); - - tested.open(executionContext); - - assertEquals("line3", tested.read()); - } - - /** - * Restart scenario - read items, rollback to last marked position, update - * execution context, create new reader and restore from restart data - the - * new input source should continue where the old one finished. - */ - @Test - public void testResetAndRestart() throws Exception { - - tested.update(executionContext); - - assertEquals("line1", tested.read()); - - assertEquals("line2", tested.read()); - - tested.update(executionContext); - - assertEquals("line3", tested.read()); - - // create new input source - tested = getItemReader(); - - tested.open(executionContext); - - assertEquals("line3", tested.read()); - } - - @Test - public void testReopen() throws Exception { - tested.update(executionContext); - - assertEquals("line1", tested.read()); - assertEquals("line2", tested.read()); - - tested.update(executionContext); - - - tested.close(executionContext); - tested.open(executionContext); - - assertEquals("line3", tested.read()); - } + private static final String FOOS = "1 \n 2 \n 3 \n 4 \n 5 \n"; - @Test - public void testRestartWithCustomRecordSeparatorPolicy() throws Exception { - - tested.setRecordSeparatorPolicy(new RecordSeparatorPolicy() { - // 1 record = 2 lines - boolean pair = true; - - public boolean isEndOfRecord(String line) { - pair = !pair; - return pair; - } - - public String postProcess(String record) { - return record; - } - - public String preProcess(String record) { - return record; + @Override + protected ItemReader getItemReader() throws Exception { + ResourceLineReader tested = new ResourceLineReader(); + Resource resource = new ByteArrayResource(FOOS.getBytes()); + tested.setResource(resource); + tested.setLineMapper(new LineMapper() { + public Foo mapLine(String line, int lineNumber) { + Foo foo = new Foo(); + foo.setValue(Integer.valueOf(line.trim())); + return foo; } }); - - tested.open(executionContext); - - assertEquals("line1line2", tested.read()); - tested.update(executionContext); - tested.close(executionContext); - tested.open(executionContext); + tested.setSaveState(true); + tested.afterPropertiesSet(); + return tested; + } + + @Override + protected void pointToEmptyInput(ItemReader tested) throws Exception { + ResourceLineReader reader = (ResourceLineReader) tested; + reader.close(new ExecutionContext()); - assertEquals("line3line4", tested.read()); + reader.setResource(new ByteArrayResource("".getBytes())); + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); } + + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/DefaultLineMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/DefaultLineMapperTests.java new file mode 100644 index 000000000..9a8cc43c8 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/DefaultLineMapperTests.java @@ -0,0 +1,39 @@ +package org.springframework.batch.item.file.mapping; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.batch.item.file.transform.LineTokenizer; + +/** + * Tests for {@link DefaultLineMapper}. + */ +public class DefaultLineMapperTests { + + private DefaultLineMapper tested = new DefaultLineMapper(); + + @Test + public void testMapping() throws Exception { + final String line = "TEST"; + final FieldSet fs = new DefaultFieldSet(new String[]{"token1", "token2"}); + final String item = "ITEM"; + + LineTokenizer tokenizer = createStrictMock(LineTokenizer.class); + expect(tokenizer.process(line)).andReturn(fs); + replay(tokenizer); + + @SuppressWarnings("unchecked") + FieldSetMapper fsMapper = createStrictMock(FieldSetMapper.class); + expect(fsMapper.process(fs)).andReturn(item); + replay(fsMapper); + + tested.setTokenizer(tokenizer); + tested.setFieldSetMapper(fsMapper); + + assertSame(item, tested.mapLine(line, 1)); + verify(tokenizer); + verify(fsMapper); + + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PassThroughLineMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PassThroughLineMapperTests.java new file mode 100644 index 000000000..5e88bb64a --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PassThroughLineMapperTests.java @@ -0,0 +1,18 @@ +package org.springframework.batch.item.file.mapping; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Tests for {@link PassThroughLineMapper}. + */ +public class PassThroughLineMapperTests { + + private PassThroughLineMapper tested = new PassThroughLineMapper(); + + @Test + public void testMapLine() throws Exception { + assertSame("line", tested.mapLine("line", 1)); + } +} diff --git a/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml b/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml index 67946e0bc..3d29adda1 100644 --- a/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml +++ b/spring-batch-samples/src/main/resources/jobs/customerFilterJob.xml @@ -34,6 +34,9 @@ + + +