diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java index b7f3bb7f7..4f88d1494 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java @@ -15,11 +15,15 @@ */ package org.springframework.batch.item.file.builder; +import java.beans.PropertyEditor; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import org.springframework.batch.item.file.FlatFileItemReader; @@ -36,11 +40,17 @@ import org.springframework.batch.item.file.transform.FieldSetFactory; import org.springframework.batch.item.file.transform.FixedLengthTokenizer; import org.springframework.batch.item.file.transform.LineTokenizer; import org.springframework.batch.item.file.transform.Range; +import org.springframework.beans.factory.BeanFactory; import org.springframework.core.io.Resource; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** + * A builder implementation for the {@link FlatFileItemReader}. + * * @author Michael Minella + * @since 4.0 + * @see FlatFileItemReader */ public class FlatFileItemReaderBuilder { @@ -71,94 +81,319 @@ public class FlatFileItemReaderBuilder { private FixedLengthBuilder fixedLengthBuilder; - private Class beanMapperClass; + private Class targetType; + + private String prototypeBeanName; + + private BeanFactory beanFactory; + + private Map, PropertyEditor> customEditors = new HashMap<>(); + + private int distanceLimit = 5; + + private boolean beanMapperStrict = true; private boolean saveState = true; - public FlatFileItemReaderBuilder strict(boolean strict) { - this.strict = strict; - return this; - } - - public FlatFileItemReaderBuilder recordSeparatorPolicy(RecordSeparatorPolicy policy) { - this.recordSeparatorPolicy = policy; - return this; - } - - public FlatFileItemReaderBuilder resource(Resource resource) { - this.resource = resource; - return this; - } - - public FlatFileItemReaderBuilder maxItemCount(int maxItemCount) { - this.maxItemCount = maxItemCount; - return this; - } - - public FlatFileItemReaderBuilder comments(String[] comments) { - this.comments.addAll(Arrays.asList(comments)); - return this; - } + private BigInteger tokenizerValidator = new BigInteger("0"); + /** + * Add a string to the list of Strings that indicate commented lines. + * + * @param comment the string to define a commented line. + * @return The current instance of the builder. + * @see FlatFileItemReader#setComments(String[]) + */ public FlatFileItemReaderBuilder addComment(String comment) { this.comments.add(comment); return this; } + /** + * An array of Strings that indicate lines that are comments (and therefore skipped by + * the reader. + * + * @param comments an array of strings to identify comments. + * @return The current instance of the builder. + * @see FlatFileItemReader#setComments(String[]) + */ + public FlatFileItemReaderBuilder comments(String[] comments) { + this.comments.addAll(Arrays.asList(comments)); + return this; + } + + /** + * Configure the max number of items to be read. + * + * @param maxItemCount the max items to be read + * @return The current instance of the builder. + * @see FlatFileItemReader#setMaxItemCount(int) + */ + public FlatFileItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + return this; + } + + /** + * Configure a custom {@link RecordSeparatorPolicy} for the reader. + * + * @param policy custom policy + * @return The current instance of the builder. + * @see FlatFileItemReader#setRecordSeparatorPolicy(RecordSeparatorPolicy) + */ + public FlatFileItemReaderBuilder recordSeparatorPolicy(RecordSeparatorPolicy policy) { + this.recordSeparatorPolicy = policy; + return this; + } + + /** + * The {@link Resource} to be used as input. + * + * @param resource the input to the reader. + * @return The current instance of the builder. + * @see FlatFileItemReader#setResource(Resource) + */ + public FlatFileItemReaderBuilder resource(Resource resource) { + this.resource = resource; + return this; + } + + /** + * Configure if the reader should be in strict mode (require the input {@link Resource} + * to exist). + * + * @param strict true if the input file is required to exist. + * @return The current instance of the builder. + * @see FlatFileItemReader#setStrict(boolean) + */ + public FlatFileItemReaderBuilder strict(boolean strict) { + this.strict = strict; + return this; + } + + /** + * The number of lines to skip at the beginning of reading the file. + * + * @param linesToSkip number of lines to be skipped. + * @return The current instance of the builder. + * @see FlatFileItemReader#setLinesToSkip(int) + */ public FlatFileItemReaderBuilder linesToSkip(int linesToSkip) { this.linesToSkip = linesToSkip; return this; } + /** + * A callback to be called for each line that is skipped. + * + * @param callback the callback + * @return The current instance of the builder. + * @see FlatFileItemReader#setSkippedLinesCallback(LineCallbackHandler) + */ public FlatFileItemReaderBuilder skippedLinesCallback(LineCallbackHandler callback) { this.skippedLinesCallback = callback; return this; } + /** + * A {@link LineMapper} implementation to be used. + * + * @param lineMapper {@link LineMapper} + * @return The current instance of the builder. + * @see FlatFileItemReader#setLineMapper(LineMapper) + */ public FlatFileItemReaderBuilder lineMapper(LineMapper lineMapper) { this.lineMapper = lineMapper; return this; } + /** + * A {@link FieldSetMapper} implementation to be used. + * + * @param mapper a {@link FieldSetMapper} + * @return The current instance of the builder. + * @see DefaultLineMapper#setFieldSetMapper(FieldSetMapper) + */ public FlatFileItemReaderBuilder fieldSetMapper(FieldSetMapper mapper) { this.fieldSetMapper = mapper; return this; } + /** + * A {@link LineTokenizer} implementation to be used. + * + * @param tokenizer a {@link LineTokenizer} + * @return The current instance of the builder. + * @see DefaultLineMapper#setLineTokenizer(LineTokenizer) + */ public FlatFileItemReaderBuilder lineTokenizer(LineTokenizer tokenizer) { + updateTokenizerValidation(tokenizer, 0); + this.lineTokenizer = tokenizer; return this; } + /** + * Returns an instance of a {@link DelimitedBuilder} for building a + * {@link DelimitedLineTokenizer}. The {@link DelimitedLineTokenizer} configured by + * this builder will only be used if one is not explicitly configured via + * {@link FlatFileItemReaderBuilder#lineTokenizer} + * + * @return a {@link DelimitedBuilder} + * + */ public DelimitedBuilder delimited() { this.delimitedBuilder = new DelimitedBuilder<>(this); + updateTokenizerValidation(this.delimitedBuilder, 1); return this.delimitedBuilder; } + /** + * Returns an instance of a {@link FixedLengthBuilder} for building a + * {@link FixedLengthTokenizer}. The {@link FixedLengthTokenizer} configured by this + * builder will only be used if the {@link FlatFileItemReaderBuilder#lineTokenizer} + * has not been configured. + * + * @return a {@link FixedLengthBuilder} + */ public FixedLengthBuilder fixedLength() { this.fixedLengthBuilder = new FixedLengthBuilder<>(this); + updateTokenizerValidation(this.fixedLengthBuilder, 2); return this.fixedLengthBuilder; } - public FlatFileItemReaderBuilder beanMapperClass(Class beanMapperClass) { - this.beanMapperClass = beanMapperClass; + /** + * The class that will represent the "item" to be returned from the reader. This + * class is used via the {@link BeanWrapperFieldSetMapper}. If more complex logic is + * required, providing your own {@link FieldSetMapper} via + * {@link FlatFileItemReaderBuilder#fieldSetMapper} is required. + * + * @param targetType The class to map to + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setTargetType(Class) + */ + public FlatFileItemReaderBuilder targetType(Class targetType) { + this.targetType = targetType; return this; } + /** + * Configures the id of a prototype scoped bean to be used as the item returned by the + * reader. + * + * @param prototypeBeanName the name of a prototype scoped bean + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setPrototypeBeanName(String) + */ + public FlatFileItemReaderBuilder prototypeBeanName(String prototypeBeanName) { + this.prototypeBeanName = prototypeBeanName; + return this; + } + + /** + * Configures the {@link BeanFactory} used to create the beans that are returned as + * items. + * + * @param beanFactory a {@link BeanFactory} + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setBeanFactory(BeanFactory) + */ + public FlatFileItemReaderBuilder beanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + return this; + } + + /** + * Register custom type converters for beans being mapped. + * + * @param customEditors a {@link Map} of editors + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setCustomEditors(Map) + */ + public FlatFileItemReaderBuilder customEditors(Map, PropertyEditor> customEditors) { + if(customEditors != null) { + this.customEditors.putAll(customEditors); + } + + return this; + } + + /** + * Configures the maximum tolerance between the actual spelling of a field's name and + * the property's name. + * + * @param distanceLimit distance limit to set + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setDistanceLimit(int) + */ + public FlatFileItemReaderBuilder distanceLimit(int distanceLimit) { + this.distanceLimit = distanceLimit; + return this; + } + + /** + * If set to true, mapping will fail if the {@link org.springframework.batch.item.file.transform.FieldSet} + * contains fields that cannot be mapped to the bean. + * + * @param beanMapperStrict defaults to false + * @return The current instance of the builder. + * @see BeanWrapperFieldSetMapper#setStrict(boolean) + */ + public FlatFileItemReaderBuilder beanMapperStrict(boolean beanMapperStrict) { + this.beanMapperStrict = beanMapperStrict; + return this; + } + + /** + * Configure if the state of the {@link FlatFileItemReader} should be persisted within + * the {@link org.springframework.batch.item.ExecutionContext} for restart purposes. + * + * @param saveState defaults to true + * @return The current instance of the builder. + * @see FlatFileItemReader#setSaveState(boolean) + */ public FlatFileItemReaderBuilder saveState(boolean saveState) { this.saveState = saveState; return this; } + /** + * The name used to calculate the key within the + * {@link org.springframework.batch.item.ExecutionContext}. Required if + * {@link FlatFileItemReaderBuilder#saveState(boolean)} is set to true. + * + * @param name name of the reader instance + * @return The current instance of the builder. + * @see FlatFileItemReader#setName(String) + */ public FlatFileItemReaderBuilder name(String name) { this.name = name; return this; } + /** + * Builds the {@link FlatFileItemReader}. + * + * @return a {@link FlatFileItemReader} + * @throws Exception + */ public FlatFileItemReader build() throws Exception { + if(this.saveState) { + Assert.state(StringUtils.hasText(this.name), + "A name is required when saveSate is set to true."); + } + Assert.notNull(this.resource, "A resource is required."); + Assert.notNull(this.recordSeparatorPolicy, "A RecordSeparatorPolicy is required."); + int validatorValue = this.tokenizerValidator.intValue(); + Assert.state(validatorValue == 1 || validatorValue == 2 || validatorValue == 4, + "Only one LineTokenizer option may be configured"); + FlatFileItemReader reader = new FlatFileItemReader<>(); - reader.setName(this.name); + if(StringUtils.hasText(this.name)) { + reader.setName(this.name); + } + reader.setResource(this.resource); if(this.lineMapper != null) { @@ -167,9 +402,8 @@ public class FlatFileItemReaderBuilder { else { DefaultLineMapper lineMapper = new DefaultLineMapper<>(); - if(this.lineTokenizer != null) { + if(this.lineTokenizer != null && this.fieldSetMapper != null) { lineMapper.setLineTokenizer(this.lineTokenizer); - lineMapper.setFieldSetMapper(this.fieldSetMapper); } else if(this.fixedLengthBuilder != null) { lineMapper.setLineTokenizer(this.fixedLengthBuilder.build()); @@ -178,22 +412,37 @@ public class FlatFileItemReaderBuilder { lineMapper.setLineTokenizer(this.delimitedBuilder.build()); } else { - throw new IllegalStateException("No LineTokenizer implementation was provided"); + throw new IllegalStateException("No LineTokenizer implementation was provided."); } - if(this.beanMapperClass != null) { - BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); - mapper.setTargetType(this.beanMapperClass); + if(this.targetType != null || StringUtils.hasText(this.prototypeBeanName)) { + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper<>(); + mapper.setTargetType(this.targetType); + mapper.setPrototypeBeanName(this.prototypeBeanName); + mapper.setStrict(this.beanMapperStrict); + mapper.setBeanFactory(this.beanFactory); + mapper.setDistanceLimit(this.distanceLimit); + mapper.setCustomEditors(this.customEditors); mapper.afterPropertiesSet(); lineMapper.setFieldSetMapper(mapper); } + else if(this.fieldSetMapper != null) { + lineMapper.setFieldSetMapper(this.fieldSetMapper); + } + else { + throw new IllegalStateException("No FieldSetMapper implementation was provided."); + } reader.setLineMapper(lineMapper); } reader.setLinesToSkip(this.linesToSkip); - reader.setComments(this.comments.toArray(new String[this.comments.size()])); + + if(!this.comments.isEmpty()) { + reader.setComments(this.comments.toArray(new String[this.comments.size()])); + } + reader.setSkippedLinesCallback(this.skippedLinesCallback); reader.setRecordSeparatorPolicy(this.recordSeparatorPolicy); reader.setMaxItemCount(this.maxItemCount); @@ -203,6 +452,20 @@ public class FlatFileItemReaderBuilder { return reader; } + private void updateTokenizerValidation(Object tokenizer, int index) { + if(tokenizer != null) { + this.tokenizerValidator = this.tokenizerValidator.flipBit(index); + } + else { + this.tokenizerValidator = this.tokenizerValidator.clearBit(index); + } + } + + /** + * A builder for constructing a {@link DelimitedLineTokenizer} + * + * @param the type of the parent {@link FlatFileItemReaderBuilder} + */ public static class DelimitedBuilder { private FlatFileItemReaderBuilder parent; @@ -218,41 +481,95 @@ public class FlatFileItemReaderBuilder { private boolean strict = true; - public DelimitedBuilder(FlatFileItemReaderBuilder parent) { + protected DelimitedBuilder(FlatFileItemReaderBuilder parent) { this.parent = parent; } + /** + * Define the delimiter for the file. + * + * @param delimiter String used as a delimiter between fields. + * @return The instance of the builder for chaining. + * @see DelimitedLineTokenizer#setDelimiter(String) + */ public DelimitedBuilder delimiter(String delimiter) { this.delimiter = delimiter; return this; } + /** + * Define the character used to quote fields. + * + * @param quoteCharacter char used to define quoted fields + * @return The instance of the builder for chaining. + * @see DelimitedLineTokenizer#setQuoteCharacter(char) + */ public DelimitedBuilder quoteCharacter(char quoteCharacter) { this.quoteCharacter = quoteCharacter; return this; } + /** + * A list of indices of the fields within a delimited file to be included + * + * @param fields indices of the fields + * @return The instance of the builder for chaining. + * @see DelimitedLineTokenizer#setIncludedFields(int[]) + */ public DelimitedBuilder includedFields(Integer[] fields) { this.includedFields.addAll(Arrays.asList(fields)); return this; } + /** + * Add an index to the list of fields to be included from the file + * + * @param field the index to be included + * @return The instance of the builder for chaining. + * @see DelimitedLineTokenizer#setIncludedFields(int[]) + */ public DelimitedBuilder addIncludedField(int field) { this.includedFields.add(field); return this; } + /** + * A factory for creating the resulting + * {@link org.springframework.batch.item.file.transform.FieldSet}. Defaults to + * {@link DefaultFieldSetFactory}. + * + * @param fieldSetFactory Factory for creating {@link org.springframework.batch.item.file.transform.FieldSet} + * @return The instance of the builder for chaining. + * @see DelimitedLineTokenizer#setFieldSetFactory(FieldSetFactory) + */ public DelimitedBuilder fieldSetFactory(FieldSetFactory fieldSetFactory) { this.fieldSetFactory = fieldSetFactory; return this; } + /** + * Names of each of the fields within the fields that are returned in the order + * they occur within the delimited file. Required. + * + * @param names names of each field + * @return The parent {@link FlatFileItemReaderBuilder} + * @see DelimitedLineTokenizer#setNames(String[]) + */ public FlatFileItemReaderBuilder names(String [] names) { this.names.addAll(Arrays.asList(names)); return this.parent; } + /** + * Returns a {@link DelimitedLineTokenizer} + * + * @return {@link DelimitedLineTokenizer} + * @throws Exception + */ public DelimitedLineTokenizer build() throws Exception { + Assert.notNull(this.fieldSetFactory, "A FieldSetFactory is required."); + Assert.notEmpty(this.names, "A list of field names is required"); + DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); tokenizer.setNames(this.names.toArray(new String[this.names.size()])); @@ -288,59 +605,111 @@ public class FlatFileItemReaderBuilder { } } + /** + * A builder for constructing a {@link FixedLengthTokenizer} + * + * @param the type of the parent {@link FlatFileItemReaderBuilder} + */ public static class FixedLengthBuilder { private FlatFileItemReaderBuilder parent; private List ranges = new ArrayList<>(); - private int maxRange = 0; - private List names = new ArrayList<>(); private boolean strict = true; private FieldSetFactory fieldSetFactory = new DefaultFieldSetFactory(); - public FixedLengthBuilder(FlatFileItemReaderBuilder parent) { + protected FixedLengthBuilder(FlatFileItemReaderBuilder parent) { this.parent = parent; } - public FixedLengthBuilder ranges(Range[] ranges) { + /** + * The column ranges for each field + * + * @param ranges column ranges + * @return This instance for chaining + * @see FixedLengthTokenizer#setColumns(Range[]) + */ + public FixedLengthBuilder columns(Range[] ranges) { this.ranges.addAll(Arrays.asList(ranges)); return this; } - public FixedLengthBuilder addRange(Range range) { + /** + * Add a column range to the existing list + * + * @param range a new column range + * @return This instance for chaining + * @see FixedLengthTokenizer#setColumns(Range[]) + */ + public FixedLengthBuilder addColumns(Range range) { this.ranges.add(range); return this; } - public FixedLengthBuilder addRange(Range range, int index) { + /** + * Insert a column range to the existing list + * + * @param range a new column range + * @param index index to add it at + * @return This instance for chaining + * @see FixedLengthTokenizer#setColumns(Range[]) + */ + public FixedLengthBuilder addColumns(Range range, int index) { this.ranges.add(index, range); return this; } - public FixedLengthBuilder maxRange(int maxRange) { - this.maxRange = maxRange; - return this; - } - + /** + * The names of the fields to be parsed from the file. Required. + * + * @param names names of fields + * @return The parent builder + * @see FixedLengthTokenizer#setNames(String[]) + */ public FlatFileItemReaderBuilder names(String [] names) { this.names.addAll(Arrays.asList(names)); return this.parent; } + /** + * Boolean indicating if the number of tokens in a line must match the number of + * fields (ranges) configured. Defaults to true. + * + * @param strict defaults to true + * @return This instance for chaining + * @see FixedLengthTokenizer#setStrict(boolean) + */ public FixedLengthBuilder strict(boolean strict) { this.strict = strict; return this; } - public FixedLengthBuilder fieldSetFactory(FieldSetFactory factory) { - this.fieldSetFactory = factory; + /** + * A factory for creating the resulting + * {@link org.springframework.batch.item.file.transform.FieldSet}. Defaults to + * {@link DefaultFieldSetFactory}. + * @param fieldSetFactory Factory for creating {@link org.springframework.batch.item.file.transform.FieldSet} + * @return The instance of the builder for chaining. + * @see FixedLengthTokenizer#setFieldSetFactory(FieldSetFactory) + */ + public FixedLengthBuilder fieldSetFactory(FieldSetFactory fieldSetFactory) { + this.fieldSetFactory = fieldSetFactory; return this; } + /** + * Returns a {@link FixedLengthTokenizer} + * + * @return a {@link FixedLengthTokenizer} + */ public FixedLengthTokenizer build() { + Assert.notNull(this.fieldSetFactory, "A FieldSetFactory is required."); + Assert.notEmpty(this.names, "A list of field names is required."); + Assert.notEmpty(this.ranges, "A list of column ranges is required."); + FixedLengthTokenizer tokenizer = new FixedLengthTokenizer(); tokenizer.setNames(this.names.toArray(new String[this.names.size()])); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java index 9e95b5fd7..8c1e97af3 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java @@ -24,7 +24,14 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.file.FlatFileItemReader; import org.springframework.batch.item.file.separator.DefaultRecordSeparatorPolicy; import org.springframework.batch.item.file.transform.DefaultFieldSet; +import org.springframework.batch.item.file.transform.FieldSet; +import org.springframework.batch.item.file.transform.FieldSetFactory; import org.springframework.batch.item.file.transform.Range; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; @@ -43,9 +50,9 @@ public class FlatFileItemReaderBuilderTests { .name("fooReader") .resource(getResource("1 2 3")) .fixedLength() - .ranges(new Range[] {new Range(1, 3), new Range(4, 6), new Range(7)}) + .columns(new Range[] {new Range(1, 3), new Range(4, 6), new Range(7)}) .names(new String[] {"first", "second", "third"}) - .beanMapperClass(Foo.class) + .targetType(Foo.class) .build(); reader.open(new ExecutionContext()); @@ -63,7 +70,7 @@ public class FlatFileItemReaderBuilderTests { .resource(getResource("1,2,3")) .delimited() .names(new String[] {"first", "second", "third"}) - .beanMapperClass(Foo.class) + .targetType(Foo.class) .build(); reader.open(new ExecutionContext()); @@ -84,7 +91,7 @@ public class FlatFileItemReaderBuilderTests { .delimited() .quoteCharacter('$') .names(new String[] {"first", "second", "third"}) - .beanMapperClass(Foo.class) + .targetType(Foo.class) .linesToSkip(1) .skippedLinesCallback(skippedLines::add) .addComment("@") @@ -119,9 +126,9 @@ public class FlatFileItemReaderBuilderTests { .name("fooReader") .resource(getResource("1 2%\n 3\n4 5%\n 6\n@this is a comment\n7 8%\n 9\n")) .fixedLength() - .ranges(new Range[] {new Range(1, 2), new Range(3, 5), new Range(6)}) + .columns(new Range[] {new Range(1, 2), new Range(3, 5), new Range(6)}) .names(new String[] {"first", "second", "third"}) - .beanMapperClass(Foo.class) + .targetType(Foo.class) .recordSeparatorPolicy(new DefaultRecordSeparatorPolicy("\"", "%")) .maxItemCount(2) .saveState(false) @@ -153,7 +160,7 @@ public class FlatFileItemReaderBuilderTests { .resource(new FileSystemResource("this/file/does/not/exist")) .delimited() .names(new String[] {"first", "second", "third"}) - .beanMapperClass(Foo.class) + .targetType(Foo.class) .strict(false) .build(); @@ -195,6 +202,156 @@ public class FlatFileItemReaderBuilderTests { assertNull(reader.read()); } + @Test + public void testComments() throws Exception { + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1,2,3\n@this is a comment\n+so is this\n4,5,6")) + .comments(new String[] {"@", "+"}) + .delimited() + .names(new String[] {"first", "second", "third"}) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + item = reader.read(); + assertEquals(4, item.getFirst()); + assertEquals(5, item.getSecond()); + assertEquals("6", item.getThird()); + assertNull(reader.read()); + } + + @Test + public void testPrototypeBean() throws Exception { + BeanFactory factory = new AnnotationConfigApplicationContext(Beans.class); + + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1,2,3")) + .delimited() + .names(new String[] {"first", "second", "third"}) + .prototypeBeanName("foo") + .beanFactory(factory) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + assertNull(reader.read()); + } + + @Test + public void testBeanWrapperFieldSetMapperStrict() throws Exception { + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1,2,3")) + .delimited() + .names(new String[] {"setFirst", "setSecond", "setThird"}) + .targetType(Foo.class) + .beanMapperStrict(true) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + assertNull(reader.read()); + } + + @Test + public void testDelimitedIncludedFields() throws Exception { + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1,2,3")) + .delimited() + .includedFields(new Integer[] {new Integer(0), new Integer(2)}) + .addIncludedField(1) + .names(new String[] {"first", "second", "third"}) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + assertNull(reader.read()); + } + + @Test + public void testDelimitedFieldSetFactory() throws Exception { + String[] names = {"first", "second", "third"}; + + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1,2,3")) + .delimited() + .fieldSetFactory(new FieldSetFactory() { + private FieldSet fieldSet = new DefaultFieldSet(new String[] {"1", "3", "foo"}, names); + + @Override + public FieldSet create(String[] values, String[] names) { + return fieldSet; + } + + @Override + public FieldSet create(String[] values) { + return fieldSet; + } + }) + .names(names) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(3, item.getSecond()); + assertEquals("foo", item.getThird()); + assertNull(reader.read()); + } + + @Test + public void testFixedLengthFieldSetFactory() throws Exception { + String[] names = {"first", "second", "third"}; + + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("fooReader") + .resource(getResource("1 2 3")) + .fixedLength() + .fieldSetFactory(new FieldSetFactory() { + private FieldSet fieldSet = new DefaultFieldSet(new String[] {"1", "3", "foo"}, names); + + @Override + public FieldSet create(String[] values, String[] names) { + return fieldSet; + } + + @Override + public FieldSet create(String[] values) { + return fieldSet; + } + }) + .columns(new Range[] {new Range(1, 3), new Range(4, 6), new Range(7)}) + .names(new String[] {"first", "second", "third"}) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(3, item.getSecond()); + assertEquals("foo", item.getThird()); + assertNull(reader.read()); + } + private Resource getResource(String contents) { return new ByteArrayResource(contents.getBytes()); } @@ -229,4 +386,14 @@ public class FlatFileItemReaderBuilderTests { } } + @Configuration + public static class Beans { + + @Bean + @Scope("prototype") + public Foo foo() { + return new Foo(); + } + } + }