Initial commit of Single Step Spring Batch Job Starter

This commit adds a new starter for creating a single step Spring Batch
job.  It allows users to configure a FlatFileItemReader and a
FlatFileItemWriter as well as the job and step with just properties.
This commit is contained in:
Michael Minella
2020-04-02 13:18:50 -05:00
committed by Glenn Renfro
parent 8b41515ee3
commit 7e76cb00f3
19 changed files with 2820 additions and 0 deletions

View File

@@ -106,6 +106,7 @@
<module>spring-cloud-task-samples</module>
<module>spring-cloud-task-integration-tests</module>
<module>spring-cloud-task-docs</module>
<module>spring-cloud-starter-single-step-batch-job</module>
</modules>
<properties>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-task-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.3.0.BUILD-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-starter-single-step-batch-job</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.Map;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.LineCallbackHandler;
import org.springframework.batch.item.file.LineMapper;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.separator.RecordSeparatorPolicy;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.batch.item.file.transform.LineTokenizer;
import org.springframework.batch.item.file.transform.Range;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Autconfiguration for a {@code FlatFileItemReader}.
*
* @author Michael Minella
* @since 2.3
*/
@Configuration
@EnableConfigurationProperties(FlatFileItemReaderProperties.class)
@AutoConfigureAfter(BatchAutoConfiguration.class)
public class FlatFileItemReaderAutoConfiguration {
private final FlatFileItemReaderProperties properties;
@Autowired(required = false)
private LineTokenizer lineTokenizer;
@Autowired(required = false)
private FieldSetMapper<Map<Object, Object>> fieldSetMapper;
@Autowired(required = false)
private LineMapper<Map<Object, Object>> lineMapper;
@Autowired(required = false)
private LineCallbackHandler skippedLinesCallback;
@Autowired(required = false)
private RecordSeparatorPolicy recordSeparatorPolicy;
public FlatFileItemReaderAutoConfiguration(FlatFileItemReaderProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job.flatfilereader", name = "name")
public FlatFileItemReader<Map<Object, Object>> itemReader() {
FlatFileItemReaderBuilder<Map<Object, Object>> mapFlatFileItemReaderBuilder = new FlatFileItemReaderBuilder<Map<Object, Object>>()
.name(this.properties.getName()).resource(this.properties.getResource())
.saveState(this.properties.isSaveState())
.maxItemCount(this.properties.getMaxItemCount())
.currentItemCount(this.properties.getCurrentItemCount())
.strict(this.properties.isStrict())
.encoding(this.properties.getEncoding())
.linesToSkip(this.properties.getLinesToSkip())
.comments(this.properties.getComments()
.toArray(new String[this.properties.getComments().size()]));
if (this.lineTokenizer != null) {
mapFlatFileItemReaderBuilder.lineTokenizer(this.lineTokenizer);
}
if (this.recordSeparatorPolicy != null) {
mapFlatFileItemReaderBuilder
.recordSeparatorPolicy(this.recordSeparatorPolicy);
}
if (this.fieldSetMapper != null) {
mapFlatFileItemReaderBuilder.fieldSetMapper(this.fieldSetMapper);
}
if (this.lineMapper != null) {
mapFlatFileItemReaderBuilder.lineMapper(this.lineMapper);
}
if (this.skippedLinesCallback != null) {
mapFlatFileItemReaderBuilder.skippedLinesCallback(skippedLinesCallback);
}
if (this.properties.isDelimited()) {
mapFlatFileItemReaderBuilder.delimited()
.quoteCharacter(this.properties.getQuoteCharacter())
.delimiter(this.properties.getDelimiter())
.includedFields(
this.properties.getIncludedFields().toArray(new Integer[0]))
.names(this.properties.getNames())
.beanMapperStrict(this.properties.isParsingStrict())
.fieldSetMapper(new MapFieldSetMapper());
}
else if (this.properties.isFixedLength()) {
mapFlatFileItemReaderBuilder.fixedLength()
.columns(this.properties.getRanges().toArray(new Range[0]))
.names(this.properties.getNames())
.fieldSetMapper(new MapFieldSetMapper())
.beanMapperStrict(this.properties.isParsingStrict());
}
return mapFlatFileItemReaderBuilder.build();
}
/**
* A {@link FieldSetMapper} that takes a {@code FieldSet} and returns the
* {@code Map<Object, Object>} of its contents.
*/
public static class MapFieldSetMapper implements FieldSetMapper<Map<Object, Object>> {
@Override
public Map<Object, Object> mapFieldSet(FieldSet fieldSet) {
return fieldSet.getProperties();
}
}
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.item.file.transform.Range;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
/**
* Properties to configure a {@code FlatFileItemReader}.
*
* @author Michael Minella
* @since 2.3
*/
@ConfigurationProperties(prefix = "spring.batch.job.flatfilereader")
public class FlatFileItemReaderProperties {
private boolean saveState = true;
private String name;
private int maxItemCount = Integer.MAX_VALUE;
private int currentItemCount = 0;
private List<String> comments = new ArrayList<>();
private Resource resource;
private boolean strict = true;
private String encoding = FlatFileItemReader.DEFAULT_CHARSET;
private int linesToSkip = 0;
private boolean delimited = false;
private String delimiter = DelimitedLineTokenizer.DELIMITER_COMMA;
private char quoteCharacter = DelimitedLineTokenizer.DEFAULT_QUOTE_CHARACTER;
private List<Integer> includedFields = new ArrayList<>();
private boolean fixedLength = false;
private List<Range> ranges = new ArrayList<>();
private String[] names;
private boolean parsingStrict = true;
/**
* Returns the configured value of if the state of the reader will be persisted.
* @return true if the state will be persisted
*/
public boolean isSaveState() {
return this.saveState;
}
/**
* Configure if the state of the
* {@link org.springframework.batch.item.ItemStreamSupport} should be persisted within
* the {@link org.springframework.batch.item.ExecutionContext} for restart purposes.
* @param saveState defaults to true
*/
public void setSaveState(boolean saveState) {
this.saveState = saveState;
}
/**
* Returns the configured value of the name used to calculate {@code ExecutionContext}
* keys.
* @return the name
*/
public String getName() {
return this.name;
}
/**
* The name used to calculate the key within the
* {@link org.springframework.batch.item.ExecutionContext}. Required if
* {@link #setSaveState} is set to true.
* @param name name of the reader instance
* @see org.springframework.batch.item.ItemStreamSupport#setName(String)
*/
public void setName(String name) {
this.name = name;
}
/**
* The maximum number of items to be read.
* @return the configured number of items, defaults to Integer.MAX_VALUE
*/
public int getMaxItemCount() {
return this.maxItemCount;
}
/**
* Configure the max number of items to be read.
* @param maxItemCount the max items to be read
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
*/
public void setMaxItemCount(int maxItemCount) {
this.maxItemCount = maxItemCount;
}
/**
* Provides the index of the current item.
* @return item index
*/
public int getCurrentItemCount() {
return this.currentItemCount;
}
/**
* Index for the current item. Also used on restarts to indicate where to start from.
* @param currentItemCount current index
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
*/
public void setCurrentItemCount(int currentItemCount) {
this.currentItemCount = currentItemCount;
}
/**
* List of {@code String} values used to indicate what records are comments.
* @return list of comment indicators
*/
public List<String> getComments() {
return this.comments;
}
/**
* Takes a list of {@code String} elements used to indicate what records are comments.
* @param comments strings used to indicate commented lines
*/
public void setComments(List<String> comments) {
this.comments = comments;
}
/**
* The input file for the {@code FlatFileItemReader}.
* @return a Resource
*/
public Resource getResource() {
return this.resource;
}
/**
* The {@link Resource} to be used as input.
* @param resource the input to the reader.
* @see FlatFileItemReader#setResource(Resource)
*/
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* Returns true if a missing input file is considered an error.
* @return true if the input file is required.
*/
public boolean isStrict() {
return this.strict;
}
/**
* 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.
* @see FlatFileItemReader#setStrict(boolean)
*/
public void setStrict(boolean strict) {
this.strict = strict;
}
/**
* Returns the encoding for the input file. Defaults to
* {@code FlatFileItemReader#DEFAULT_CHARSET}.
* @return the configured encoding
*/
public String getEncoding() {
return this.encoding;
}
/**
* Configure the encoding used by the reader to read the input source. Default value
* is {@link FlatFileItemReader#DEFAULT_CHARSET}.
* @param encoding to use to read the input source.
* @see FlatFileItemReader#setEncoding(String)
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Number of lines to skip when reading the input file.
* @return number of lines
*/
public int getLinesToSkip() {
return this.linesToSkip;
}
/**
* The number of lines to skip at the beginning of reading the file.
* @param linesToSkip number of lines to be skipped.
* @see FlatFileItemReader#setLinesToSkip(int)
*/
public void setLinesToSkip(int linesToSkip) {
this.linesToSkip = linesToSkip;
}
/**
* Indicates if the input file is a delimited file or not.
* @return true if the file is delimited
*/
public boolean isDelimited() {
return this.delimited;
}
/**
* Indicates that a {@link DelimitedLineTokenizer} should be used to parse each line.
* @param delimited true if the file is a delimited file
*/
public void setDelimited(boolean delimited) {
this.delimited = delimited;
}
/**
* The {@code String} used to divide the record into fields.
* @return the delimiter
*/
public String getDelimiter() {
return this.delimiter;
}
/**
* Define the delimiter for the file.
* @param delimiter String used as a delimiter between fields.
* @see DelimitedLineTokenizer#setDelimiter(String)
*/
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
/**
* The char used to indicate that a field is quoted.
* @return the quote char
*/
public char getQuoteCharacter() {
return this.quoteCharacter;
}
/**
* Define the character used to quote fields.
* @param quoteCharacter char used to define quoted fields
* @see DelimitedLineTokenizer#setQuoteCharacter(char)
*/
public void setQuoteCharacter(char quoteCharacter) {
this.quoteCharacter = quoteCharacter;
}
/**
* A {@code List} of indices indicating what fields to include.
* @return list of indices
*/
public List<Integer> getIncludedFields() {
return this.includedFields;
}
/**
* A list of indices of the fields within a delimited file to be included.
* @param includedFields indices of the fields
* @see DelimitedLineTokenizer#setIncludedFields(int[])
*/
public void setIncludedFields(List<Integer> includedFields) {
this.includedFields = includedFields;
}
/**
* Indicates that a file contains records with fixed length columns.
* @return true if the file is parsed using column indices
*/
public boolean isFixedLength() {
return this.fixedLength;
}
/**
* Indicates that a
* {@link org.springframework.batch.item.file.transform.FixedLengthTokenizer} should
* be used to parse the records in the file.
* @param fixedLength true if the records should be tokenized by column index
*/
public void setFixedLength(boolean fixedLength) {
this.fixedLength = fixedLength;
}
/**
* The column ranges to be used to parsed a fixed width file.
* @return a list of {@link Range} instances
*/
public List<Range> getRanges() {
return this.ranges;
}
/**
* Column ranges for each field.
* @param ranges list of ranges in start-end format (end is optional)
*/
public void setRanges(List<Range> ranges) {
this.ranges = ranges;
}
/**
* Names of each column.
* @return names
*/
public String[] getNames() {
return this.names;
}
/**
* The names of the fields to be parsed from the file.
* @param names names of fields
*/
public void setNames(String[] names) {
this.names = names;
}
/**
* Indicates if the number of tokens must match the number of configured fields.
* @return true if they must match
*/
public boolean isParsingStrict() {
return this.parsingStrict;
}
/**
* Indicates if the number of tokens must match the number of configured fields.
* @param parsingStrict true if they must match
*/
public void setParsingStrict(boolean parsingStrict) {
this.parsingStrict = parsingStrict;
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.batch.item.file.FlatFileFooterCallback;
import org.springframework.batch.item.file.FlatFileHeaderCallback;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
import org.springframework.batch.item.file.transform.FieldExtractor;
import org.springframework.batch.item.file.transform.LineAggregator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Autoconfiguration for a {@code FlatFileItemWriter}.
*
* @author Michael Minella
* @since 2.3
*/
@Configuration
@EnableConfigurationProperties(FlatFileItemWriterProperties.class)
@AutoConfigureAfter(BatchAutoConfiguration.class)
public class FlatFileItemWriterAutoConfiguration {
private FlatFileItemWriterProperties properties;
@Autowired(required = false)
private LineAggregator<Map<Object, Object>> lineAggregator;
@Autowired(required = false)
private FieldExtractor<Map<Object, Object>> fieldExtractor;
@Autowired(required = false)
private FlatFileHeaderCallback headerCallback;
@Autowired(required = false)
private FlatFileFooterCallback footerCallback;
public FlatFileItemWriterAutoConfiguration(FlatFileItemWriterProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job.flatfilewriter", name = "name")
public FlatFileItemWriter<Map<Object, Object>> itemWriter() {
if (this.properties.isDelimited() && this.properties.isFormatted()) {
throw new IllegalStateException(
"An output file must be either delimited or formatted or a custom "
+ "LineAggregator must be provided. Your current configuration specifies both delimited and formatted");
}
else if ((this.properties.isFormatted() || this.properties.isDelimited())
&& this.lineAggregator != null) {
throw new IllegalStateException("A LineAggregator must be configured if the "
+ "output is not formatted or delimited");
}
FlatFileItemWriterBuilder<Map<Object, Object>> builder = new FlatFileItemWriterBuilder<Map<Object, Object>>()
.name(this.properties.getName()).resource(this.properties.getResource())
.append(this.properties.isAppend())
.encoding(this.properties.getEncoding())
.forceSync(this.properties.isForceSync())
.lineSeparator(this.properties.getLineSeparator())
.saveState(this.properties.isSaveState())
.shouldDeleteIfEmpty(this.properties.isShouldDeleteIfEmpty())
.shouldDeleteIfExists(this.properties.isShouldDeleteIfExists())
.transactional(this.properties.isTransactional())
.headerCallback(this.headerCallback).footerCallback(this.footerCallback);
if (this.properties.isDelimited()) {
FlatFileItemWriterBuilder.DelimitedBuilder<Map<Object, Object>> delimitedBuilder = builder
.delimited().delimiter(this.properties.getDelimiter());
if (this.fieldExtractor != null) {
delimitedBuilder.fieldExtractor(this.fieldExtractor);
}
else {
delimitedBuilder.fieldExtractor(
new MapFieldExtractor(this.properties.getNames()));
}
}
else if (this.properties.isFormatted()) {
FlatFileItemWriterBuilder.FormattedBuilder<Map<Object, Object>> formattedBuilder = builder
.formatted().format(this.properties.getFormat())
.locale(this.properties.getLocale())
.maximumLength(this.properties.getMaximumLength())
.minimumLength(this.properties.getMinimumLength());
if (this.fieldExtractor != null) {
formattedBuilder.fieldExtractor(this.fieldExtractor);
}
else {
formattedBuilder.fieldExtractor(
new MapFieldExtractor(this.properties.getNames()));
}
}
else if (this.lineAggregator != null) {
builder.lineAggregator(this.lineAggregator);
}
return builder.build();
}
/**
* A {@code FieldExtractor} that converts a {@code Map<Object, Object>} to the ordered
* {@code Object[]} required to populate an output record.
*/
public static class MapFieldExtractor implements FieldExtractor<Map<Object, Object>> {
private String[] names;
public MapFieldExtractor(String[] names) {
this.names = names;
}
@Override
public Object[] extract(Map<Object, Object> item) {
List<Object> fields = new ArrayList<>(item.size());
for (String name : this.names) {
fields.add(item.get(name));
}
return fields.toArray();
}
}
}

View File

@@ -0,0 +1,381 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.Locale;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
/**
* Properties for configuring a {@code FlatFileItemWriter}.
*
* @author Michael Minella
* @since 2.3
*/
@ConfigurationProperties(prefix = "spring.batch.job.flatfilewriter")
public class FlatFileItemWriterProperties {
private Resource resource;
private boolean delimited;
private boolean formatted;
private String format;
private Locale locale = Locale.getDefault();
private int maximumLength = 0;
private int minimumLength = 0;
private String delimiter = ",";
private String encoding = FlatFileItemWriter.DEFAULT_CHARSET;
private boolean forceSync = false;
private String[] names;
private boolean append = false;
private String lineSeparator = FlatFileItemWriter.DEFAULT_LINE_SEPARATOR;
private String name;
private boolean saveState = true;
private boolean shouldDeleteIfEmpty = false;
private boolean shouldDeleteIfExists = true;
private boolean transactional = FlatFileItemWriter.DEFAULT_TRANSACTIONAL;
/**
* Returns the configured value of if the state of the reader will be persisted.
* @return true if the state will be persisted
*/
public boolean isSaveState() {
return this.saveState;
}
/**
* Configure if the state of the
* {@link org.springframework.batch.item.ItemStreamSupport} should be persisted within
* the {@link org.springframework.batch.item.ExecutionContext} for restart purposes.
* @param saveState defaults to true
*/
public void setSaveState(boolean saveState) {
this.saveState = saveState;
}
/**
* Returns the configured value of the name used to calculate {@code ExecutionContext}
* keys.
* @return the name
*/
public String getName() {
return this.name;
}
/**
* The name used to calculate the key within the
* {@link org.springframework.batch.item.ExecutionContext}. Required if
* {@link #setSaveState} is set to true.
* @param name name of the reader instance
* @see org.springframework.batch.item.ItemStreamSupport#setName(String)
*/
public void setName(String name) {
this.name = name;
}
/**
* The output file for the {@code FlatFileItemWriter}.
* @return a {@code Resource}
*/
public Resource getResource() {
return this.resource;
}
/**
* The {@link Resource} to be used as output.
* @param resource the input to the reader.
* @see FlatFileItemWriter#setResource
*/
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* Indicates of the output will be delimited by a configured string (, by default).
* @return true if the output file is a delimited file
*/
public boolean isDelimited() {
return delimited;
}
/**
* Configure the use of the {@code DelimitedLineAggregator} to generate the output per
* item.
* @param delimited indicator if the file will be delimited or not
*/
public void setDelimited(boolean delimited) {
this.delimited = delimited;
}
/**
* . When a file is delimited, this {@code String} will be used as the delimiter
* between fields
* @return delimiter
*/
public String getDelimiter() {
return delimiter;
}
/**
* Configure the {@code String} used to delimit the fields in the output file.
* @param delimiter {@code String} used to delimit the fields of the output file.
*/
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
/**
* Names of the fields to be extracted into the output.
* @return An array of field names
*/
public String[] getNames() {
return names;
}
/**
* Provide an ordered array of field names used to generate the output of a file.
* @param names An array of field names
*/
public void setNames(String[] names) {
this.names = names;
}
/**
* True if an output file is found and should be added onto instead of
* replaced/deleted. False by default.
* @return appending indicator
*/
public boolean isAppend() {
return append;
}
/**
* Configure if the output file is found if it should be appended to. Defaults to
* false.
* @param append true if the output file should be appended onto if found.
*/
public void setAppend(boolean append) {
this.append = append;
}
/**
* Indicates that the output file will use String formatting to generate the output.
* @return true if the file will contain formatted records
*/
public boolean isFormatted() {
return formatted;
}
/**
* Indicates to use a {@code FormatterLineAggregator} to generate the output per item.
* @param formatted true if the output should be formatted via the
* {@code FormatterLineAggregator}
*/
public void setFormatted(boolean formatted) {
this.formatted = formatted;
}
/**
* File encoding for the output file.
* @return the configured encoding for the output file (Defaults to
* {@code FlatFileItemWriter.DEFAULT_CHARSET})
*/
public String getEncoding() {
return encoding;
}
/**
* Configure encoding of the output file.
* @param encoding output encoding
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* A flag indicating that changes should be force-synced to disk on flush. Defaults to
* false.
* @return The current instance of the builder.
*/
public boolean isForceSync() {
return forceSync;
}
/**
* A flag indicating that changes should be force-synced to disk on flush. Defaults to
* false.
* @param forceSync value to set the flag to
*/
public void setForceSync(boolean forceSync) {
this.forceSync = forceSync;
}
/**
* String used to separate lines in output. Defaults to the System property
* line.separator.
* @return the separator string
*/
public String getLineSeparator() {
return lineSeparator;
}
/**
* Configure the {@code String} used to separate each line.
* @param lineSeparator defaults to System's line.separator property
*/
public void setLineSeparator(String lineSeparator) {
this.lineSeparator = lineSeparator;
}
/**
* Indicates if the output file should be deleted if no output was written to it.
* Defaults to false.
* @return true if a file that is empty at the end of the step should be deleted.
*/
public boolean isShouldDeleteIfEmpty() {
return shouldDeleteIfEmpty;
}
/**
* Configure if an empty output file should be deleted once the step is complete.
* Defaults to false.
* @param shouldDeleteIfEmpty true if the file should be deleted if no items have been
* written to it.
*/
public void setShouldDeleteIfEmpty(boolean shouldDeleteIfEmpty) {
this.shouldDeleteIfEmpty = shouldDeleteIfEmpty;
}
/**
* Indicates if an existing output file should be deleted on startup. Defaults to
* true.
* @return if an existing output file should be deleted.
*/
public boolean isShouldDeleteIfExists() {
return shouldDeleteIfExists;
}
/**
* Configures if an existing output file should be deleted on the start of the step.
* Defaults to true.
* @param shouldDeleteIfExists if true and an output file of a previous run is found,
* it will be deleted.
*/
public void setShouldDeleteIfExists(boolean shouldDeleteIfExists) {
this.shouldDeleteIfExists = shouldDeleteIfExists;
}
/**
* Indicates if flushing the buffer should be delayed while a transaction is active.
* Defaults to true.
* @return flag indicating if flushing should be delayed during a transaction
*/
public boolean isTransactional() {
return transactional;
}
/**
* Configure if output should not be flushed to disk during an active transaction.
* @param transactional defaults to true
*/
public void setTransactional(boolean transactional) {
this.transactional = transactional;
}
/**
* Format used with the {@code FormatterLineAggregator}.
* @return the format for each item's output.
*/
public String getFormat() {
return format;
}
/**
* Configure the format the {@code FormatterLineAggregator} will use for each item.
* @param format the format for each item's output.
*/
public void setFormat(String format) {
this.format = format;
}
/**
* The {@code Locale} used when generating the output file.
* @return configured {@code Locale}. Defaults to {@code Locale.getDefault()}
*/
public Locale getLocale() {
return locale;
}
/**
* Configure the {@code Locale} to use when generating the output.
* @param locale the configured {@code Locale}
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
/**
* The longest a record is allowed to be. If 0, the maximum is unlimited.
* @return the max record length allowed
*/
public int getMaximumLength() {
return maximumLength;
}
/**
* Configure the maximum record length. If 0, the size is unbounded.
* @param maximumLength the maximum record length allowed.
*/
public void setMaximumLength(int maximumLength) {
this.maximumLength = maximumLength;
}
/**
* The minimum record length.
* @return the minimum record length allowed.
*/
public int getMinimumLength() {
return minimumLength;
}
/**
* Configure the minimum record length.
* @param minimumLength the minimum record length.
*/
public void setMinimumLength(int minimumLength) {
this.minimumLength = minimumLength;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import org.springframework.batch.item.file.transform.Range;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
/**
* Converter for taking properties of format {@code start-end} or {@code start} (where
* start and end are both integers) and converting them into {@link Range} instances for
* configuring a {@link org.springframework.batch.item.file.FlatFileItemReader}.
*
* @author Michael Minella
* @since 2.3
*/
@Component
@ConfigurationPropertiesBinding
public class RangeConverter implements Converter<String, Range> {
@Override
public Range convert(String source) {
if (source == null) {
return null;
}
String[] columns = source.split("-");
if (columns.length == 1) {
int start = Integer.parseInt(columns[0]);
return new Range(start);
}
else if (columns.length == 2) {
int start = Integer.parseInt(columns[0]);
int end = Integer.parseInt(columns[1]);
return new Range(start, end);
}
else {
throw new IllegalArgumentException(String.format(
"%s is in an illegal format. Ranges must be specified as startIndex-endIndex",
source));
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.Map;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.step.builder.SimpleStepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* Autoconfiguration to create a single step Spring Batch Job.
*
* @author Michael Minella
* @since 2.3
*/
@Configuration
@EnableConfigurationProperties(SingleStepJobProperties.class)
@AutoConfigureAfter(BatchAutoConfiguration.class)
public class SingleStepJobAutoConfiguration {
private JobBuilderFactory jobBuilderFactory;
private StepBuilderFactory stepBuilderFactory;
private SingleStepJobProperties properties;
@Autowired(required = false)
private ItemProcessor<Map<Object, Object>, Map<Object, Object>> itemProcessor;
public SingleStepJobAutoConfiguration(JobBuilderFactory jobBuilderFactory,
StepBuilderFactory stepBuilderFactory, SingleStepJobProperties properties,
ApplicationContext context) {
validateProperties(properties);
this.jobBuilderFactory = jobBuilderFactory;
this.stepBuilderFactory = stepBuilderFactory;
this.properties = properties;
}
private void validateProperties(SingleStepJobProperties properties) {
Assert.hasText(properties.getJobName(), "A job name is required");
Assert.hasText(properties.getStepName(), "A step name is required");
Assert.notNull(properties.getChunkSize(), "A chunk size is required");
Assert.isTrue(properties.getChunkSize() > 0,
"A chunk size greater than zero is required");
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job", name = "jobName")
public Job job(ItemReader<Map<Object, Object>> itemReader,
ItemWriter<Map<Object, Object>> itemWriter) {
SimpleStepBuilder<Map<Object, Object>, Map<Object, Object>> stepBuilder = this.stepBuilderFactory
.get(this.properties.getStepName())
.<Map<Object, Object>, Map<Object, Object>>chunk(
this.properties.getChunkSize())
.reader(itemReader);
stepBuilder.processor(this.itemProcessor);
Step step = stepBuilder.writer(itemWriter).build();
return this.jobBuilderFactory.get(this.properties.getJobName()).start(step)
.build();
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2019-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties to configure the step and job level properties for a single step job.
*
* @author Michael Minella
* @since 2.3
*/
@ConfigurationProperties(prefix = "spring.batch.job")
public class SingleStepJobProperties {
private String stepName;
private Integer chunkSize;
private String jobName;
/**
* Name of the step in the single step job.
* @return name
*/
public String getStepName() {
return stepName;
}
/**
* Set the name of the step.
* @param stepName name
*/
public void setStepName(String stepName) {
this.stepName = stepName;
}
/**
* The number of items to process per transaction/chunk.
* @return number of items
*/
public Integer getChunkSize() {
return chunkSize;
}
/**
* Set the number of items within a transaction/chunk.
* @param chunkSize number of items
*/
public void setChunkSize(Integer chunkSize) {
this.chunkSize = chunkSize;
}
/**
* The name of the job.
* @return name
*/
public String getJobName() {
return jobName;
}
/**
* Set the name of the job.
* @param jobName name
*/
public void setJobName(String jobName) {
this.jobName = jobName;
}
}

View File

@@ -0,0 +1,229 @@
{
"groups": [
{
"name": "spring.batch.job",
"type": "org.springframework.cloud.task.batch.autoconfigure.SingleStepJobProperties",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.SingleStepJobProperties"
},
{
"name": "spring.batch.job.flatfilereader",
"type": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilewriter",
"type": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
}
],
"properties": [
{
"name": "spring.batch.job.chunk-size",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.SingleStepJobProperties"
},
{
"name": "spring.batch.job.flatfilereader.comments",
"type": "java.util.List<java.lang.String>",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.current-item-count",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": 0
},
{
"name": "spring.batch.job.flatfilereader.delimited",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilereader.delimiter",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.encoding",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.fixed-length",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilereader.included-fields",
"type": "java.util.List<java.lang.Integer>",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.lines-to-skip",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": 0
},
{
"name": "spring.batch.job.flatfilereader.max-item-count",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.name",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.names",
"type": "java.lang.String[]",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.parsing-strict",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": true
},
{
"name": "spring.batch.job.flatfilereader.quote-character",
"type": "java.lang.Character",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.ranges",
"type": "java.util.List<org.springframework.batch.item.file.transform.Range>",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.resource",
"type": "org.springframework.core.io.Resource",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties"
},
{
"name": "spring.batch.job.flatfilereader.save-state",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": true
},
{
"name": "spring.batch.job.flatfilereader.strict",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderProperties",
"defaultValue": true
},
{
"name": "spring.batch.job.flatfilewriter.append",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilewriter.delimited",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilewriter.delimiter",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": ","
},
{
"name": "spring.batch.job.flatfilewriter.encoding",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.force-sync",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilewriter.format",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.formatted",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilewriter.line-separator",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.locale",
"type": "java.util.Locale",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.maximum-length",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": 0
},
{
"name": "spring.batch.job.flatfilewriter.minimum-length",
"type": "java.lang.Integer",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": 0
},
{
"name": "spring.batch.job.flatfilewriter.name",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.names",
"type": "java.lang.String[]",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.resource",
"type": "org.springframework.core.io.Resource",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.flatfilewriter.save-state",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": true
},
{
"name": "spring.batch.job.flatfilewriter.should-delete-if-empty",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": false
},
{
"name": "spring.batch.job.flatfilewriter.should-delete-if-exists",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties",
"defaultValue": true
},
{
"name": "spring.batch.job.flatfilewriter.transactional",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterProperties"
},
{
"name": "spring.batch.job.job-name",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.SingleStepJobProperties"
},
{
"name": "spring.batch.job.step-name",
"type": "java.lang.String",
"sourceType": "org.springframework.cloud.task.batch.autoconfigure.SingleStepJobProperties"
}
],
"hints": []
}

View File

@@ -0,0 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.autoconfigure.FlatFileItemReaderAutoConfiguration,\
org.springframework.cloud.task.batch.autoconfigure.RangeConverter,\
org.springframework.cloud.task.batch.autoconfigure.SingleStepJobAutoConfiguration,\
org.springframework.cloud.task.batch.autoconfigure.FlatFileItemWriterAutoConfiguration

View File

@@ -0,0 +1,469 @@
/*
* Copyright 2020-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.LineCallbackHandler;
import org.springframework.batch.item.file.LineMapper;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.separator.RecordSeparatorPolicy;
import org.springframework.batch.item.file.transform.DefaultFieldSet;
import org.springframework.batch.item.file.transform.LineTokenizer;
import org.springframework.batch.item.support.ListItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Michael Minella
*/
public class FlatFileItemReaderAutoConfigurationTests {
/**
* Contents of the file to be read (included here because it's UTF-16):
*
* <pre>
* 1@2@3@4@5@six
* # This should be ignored
* 7@8@9@10@11@twelve
* $ So should this
* 13@14@15@16@17@eighteen
* 19@20@21@22@23@%twenty four%
* 15@26@27@28@29@thirty
* 31@32@33@34@35@thirty six
* 37@38@39@40@41@forty two
* 43@44@45@46@47@forty eight
* 49@50@51@52@53@fifty four
* 55@56@57@58@59@sixty
* </pre>
*/
@Test
public void testFullDelimitedConfiguration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(JobConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemReaderAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilereader.savestate=true",
"spring.batch.job.flatfilereader.name=fullDelimitedConfiguration",
"spring.batch.job.flatfilereader.maxItemCount=5",
"spring.batch.job.flatfilereader.currentItemCount=2",
"spring.batch.job.flatfilereader.comments=#,$",
"spring.batch.job.flatfilereader.resource=/testUTF16.csv",
"spring.batch.job.flatfilereader.strict=true",
"spring.batch.job.flatfilereader.encoding=UTF-16",
"spring.batch.job.flatfilereader.linesToSkip=1",
"spring.batch.job.flatfilereader.delimited=true",
"spring.batch.job.flatfilereader.delimiter=@",
"spring.batch.job.flatfilereader.quoteCharacter=%",
"spring.batch.job.flatfilereader.includedFields=1,3,5",
"spring.batch.job.flatfilereader.names=foo,bar,baz",
"spring.batch.job.flatfilereader.parsingStrict=false");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
List writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(3);
assertThat(((Map) writtenItems.get(0)).get("foo")).isEqualTo("20");
assertThat(((Map) writtenItems.get(0)).get("bar")).isEqualTo("22");
assertThat(((Map) writtenItems.get(0)).get("baz")).isEqualTo("twenty four");
assertThat(((Map) writtenItems.get(1)).get("foo")).isEqualTo("26");
assertThat(((Map) writtenItems.get(1)).get("bar")).isEqualTo("28");
assertThat(((Map) writtenItems.get(1)).get("baz")).isEqualTo("thirty");
assertThat(((Map) writtenItems.get(2)).get("foo")).isEqualTo("32");
assertThat(((Map) writtenItems.get(2)).get("bar")).isEqualTo("34");
assertThat(((Map) writtenItems.get(2)).get("baz")).isEqualTo("thirty six");
});
}
@Test
public void testFixedWidthConfiguration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(JobConfiguration.class)
.withConfiguration(AutoConfigurations.of(
PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemReaderAutoConfiguration.class, RangeConverter.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilereader.savestate=true",
"spring.batch.job.flatfilereader.name=fixedWidthConfiguration",
"spring.batch.job.flatfilereader.comments=#,$",
"spring.batch.job.flatfilereader.resource=/test.txt",
"spring.batch.job.flatfilereader.strict=true",
"spring.batch.job.flatfilereader.fixedLength=true",
"spring.batch.job.flatfilereader.ranges=3-4,7-8,11",
"spring.batch.job.flatfilereader.names=foo,bar,baz",
"spring.batch.job.flatfilereader.parsingStrict=false");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
List writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(6);
assertThat(((Map) writtenItems.get(0)).get("foo")).isEqualTo("2");
assertThat(((Map) writtenItems.get(0)).get("bar")).isEqualTo("4");
assertThat(((Map) writtenItems.get(0)).get("baz")).isEqualTo("six");
assertThat(((Map) writtenItems.get(1)).get("foo")).isEqualTo("8");
assertThat(((Map) writtenItems.get(1)).get("bar")).isEqualTo("10");
assertThat(((Map) writtenItems.get(1)).get("baz")).isEqualTo("twelve");
assertThat(((Map) writtenItems.get(2)).get("foo")).isEqualTo("14");
assertThat(((Map) writtenItems.get(2)).get("bar")).isEqualTo("16");
assertThat(((Map) writtenItems.get(2)).get("baz")).isEqualTo("eighteen");
assertThat(((Map) writtenItems.get(3)).get("foo")).isEqualTo("20");
assertThat(((Map) writtenItems.get(3)).get("bar")).isEqualTo("22");
assertThat(((Map) writtenItems.get(3)).get("baz")).isEqualTo("twenty four");
assertThat(((Map) writtenItems.get(4)).get("foo")).isEqualTo("26");
assertThat(((Map) writtenItems.get(4)).get("bar")).isEqualTo("28");
assertThat(((Map) writtenItems.get(4)).get("baz")).isEqualTo("thirty");
assertThat(((Map) writtenItems.get(5)).get("foo")).isEqualTo("32");
assertThat(((Map) writtenItems.get(5)).get("bar")).isEqualTo("34");
assertThat(((Map) writtenItems.get(5)).get("baz")).isEqualTo("thirty six");
});
}
@Test
public void testCustomLineMapper() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(CustomLineMapperConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemReaderAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilereader.name=fixedWidthConfiguration",
"spring.batch.job.flatfilereader.resource=/test.txt",
"spring.batch.job.flatfilereader.strict=true");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
List writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(8);
});
}
/**
* This test requires an input file with an even number of records
*/
@Test
public void testCustomRecordSeparatorAndSkippedLines() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(
RecordSeparatorAndSkippedLinesJobConfiguration.class)
.withConfiguration(AutoConfigurations.of(
PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemReaderAutoConfiguration.class, RangeConverter.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilereader.name=fixedWidthConfiguration",
"spring.batch.job.flatfilereader.resource=/test.txt",
"spring.batch.job.flatfilereader.linesToSkip=2",
"spring.batch.job.flatfilereader.fixedLength=true",
"spring.batch.job.flatfilereader.ranges=3-4,7-8,11",
"spring.batch.job.flatfilereader.names=foo,bar,baz",
"spring.batch.job.flatfilereader.strict=true");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
ListLineCallbackHandler callbackHandler = context
.getBean(ListLineCallbackHandler.class);
assertThat(callbackHandler.getLines().size()).isEqualTo(2);
List writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(2);
});
}
@Test
public void testCustomMapping() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(CustomMappingConfiguration.class)
.withConfiguration(AutoConfigurations.of(
PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemReaderAutoConfiguration.class, RangeConverter.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilereader.name=fixedWidthConfiguration",
"spring.batch.job.flatfilereader.resource=/test.txt",
"spring.batch.job.flatfilereader.maxItemCount=1",
"spring.batch.job.flatfilereader.strict=true");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
List<Map<Object, Object>> writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(1);
assertThat(writtenItems.get(0).get("one")).isEqualTo("1 2 3");
assertThat(writtenItems.get(0).get("two")).isEqualTo("4 5 six");
});
}
@EnableBatchProcessing
@Configuration
public static class CustomMappingConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private FlatFileItemReader itemReader;
@Bean
public ListItemWriter<Map> itemWriter() {
return new ListItemWriter<>();
}
@Bean
public LineTokenizer lineTokenizer() {
return line -> new DefaultFieldSet(
new String[] { line.substring(0, 5), line.substring(6) },
new String[] { "one", "two" });
}
@Bean
public FieldSetMapper<Map<Object, Object>> fieldSetMapper() {
return fieldSet -> fieldSet.getProperties();
}
}
@EnableBatchProcessing
@Configuration
public static class JobConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private FlatFileItemReader itemReader;
@Bean
public ListItemWriter<Map> itemWriter() {
return new ListItemWriter<>();
}
}
@EnableBatchProcessing
@Configuration
public static class RecordSeparatorAndSkippedLinesJobConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private FlatFileItemReader itemReader;
@Bean
public RecordSeparatorPolicy recordSeparatorPolicy() {
return new RecordSeparatorPolicy() {
@Override
public boolean isEndOfRecord(String record) {
boolean endOfRecord = false;
int index = record.indexOf('\n');
if (index > 0 && record.length() > index + 1) {
endOfRecord = true;
}
return endOfRecord;
}
@Override
public String postProcess(String record) {
return record;
}
@Override
public String preProcess(String record) {
return record + '\n';
}
};
}
@Bean
public LineCallbackHandler lineCallbackHandler() {
return new ListLineCallbackHandler();
}
@Bean
public ListItemWriter<Map> itemWriter() {
return new ListItemWriter<>();
}
}
@EnableBatchProcessing
@Configuration
public static class CustomLineMapperConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private FlatFileItemReader itemReader;
@Bean
public LineMapper<Map<Object, Object>> lineMapper() {
return (line, lineNumber) -> {
Map<Object, Object> item = new HashMap<>(1);
item.put("line", line);
item.put("lineNumber", lineNumber);
return item;
};
}
@Bean
public ListItemWriter<Map> itemWriter() {
return new ListItemWriter<>();
}
}
public static class ListLineCallbackHandler implements LineCallbackHandler {
private List<String> lines = new ArrayList<>();
@Override
public void handleLine(String line) {
lines.add(line);
}
public List<String> getLines() {
return lines;
}
}
}

View File

@@ -0,0 +1,532 @@
/*
* Copyright 2020-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.io.File;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.file.FlatFileFooterCallback;
import org.springframework.batch.item.file.FlatFileHeaderCallback;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.transform.FieldExtractor;
import org.springframework.batch.item.file.transform.LineAggregator;
import org.springframework.batch.item.file.transform.PassThroughLineAggregator;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.test.AssertFile;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Michael Minella
*/
public class FlatFileItemWriterAutoConfigurationTests {
private File outputFile;
@Before
public void setUp() throws Exception {
this.outputFile = File.createTempFile("flatfile-config-test-output", ".tmp");
}
@After
public void tearDown() {
this.outputFile.delete();
}
@Test
public void testValidation() {
FlatFileItemWriterProperties properties = new FlatFileItemWriterProperties();
properties.setFormatted(true);
properties.setDelimited(true);
FlatFileItemWriterAutoConfiguration configuration = new FlatFileItemWriterAutoConfiguration(
properties);
try {
configuration.itemWriter();
}
catch (IllegalStateException ise) {
assertThat(ise.getMessage()).isEqualTo(
"An output file must be either delimited or formatted or a custom "
+ "LineAggregator must be provided. Your current configuration specifies both delimited and formatted");
}
catch (Exception e) {
fail("Incorrect exception thrown", e);
}
properties.setFormatted(true);
properties.setDelimited(false);
ReflectionTestUtils.setField(configuration, "lineAggregator",
new PassThroughLineAggregator<>());
try {
configuration.itemWriter();
}
catch (IllegalStateException ise) {
assertThat(ise.getMessage())
.isEqualTo("A LineAggregator must be configured if the "
+ "output is not formatted or delimited");
}
catch (Exception e) {
fail("Incorrect exception thrown", e);
}
properties.setFormatted(false);
properties.setDelimited(true);
try {
configuration.itemWriter();
}
catch (IllegalStateException ise) {
assertThat(ise.getMessage())
.isEqualTo("A LineAggregator must be configured if the "
+ "output is not formatted or delimited");
}
catch (Exception e) {
fail("Incorrect exception thrown", e);
}
}
@Test
public void testDelimitedFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(DelimitedJobConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-16",
"spring.batch.job.flatfilewriter.saveState=false",
"spring.batch.job.flatfilewriter.shouldDeleteIfEmpty=true",
"spring.batch.job.flatfilewriter.delimited=true",
"spring.batch.job.flatfilewriter.names=item",
"spring.batch.job.flatfilewriter.append=true",
"spring.batch.job.flatfilewriter.forceSync=true",
"spring.batch.job.flatfilewriter.shouldDeleteIfExists=false",
"spring.batch.job.flatfilewriter.transactional=false");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
FlatFileItemWriter writer = context.getBean(FlatFileItemWriter.class);
AssertFile.assertLineCount(3, this.outputFile);
AssertFile.assertFileEquals(new ClassPathResource("writerTestUTF16.txt"),
new FileSystemResource(this.outputFile));
assertThat((Boolean) ReflectionTestUtils.getField(writer, "saveState"))
.isFalse();
assertThat((Boolean) ReflectionTestUtils.getField(writer, "append")).isTrue();
assertThat((Boolean) ReflectionTestUtils.getField(writer, "forceSync"))
.isTrue();
assertThat((Boolean) ReflectionTestUtils.getField(writer,
"shouldDeleteIfExists")).isFalse();
assertThat((Boolean) ReflectionTestUtils.getField(writer, "transactional"))
.isFalse();
});
}
@Test
public void testFormattedFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(FormattedJobConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=2",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-8",
"spring.batch.job.flatfilewriter.formatted=true",
"spring.batch.job.flatfilewriter.names=item",
"spring.batch.job.flatfilewriter.format=item = %s",
"spring.batch.job.flatfilewriter.minimumLength=8",
"spring.batch.job.flatfilewriter.maximumLength=10");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
AssertFile.assertLineCount(2, this.outputFile);
String results = FileCopyUtils.copyToString(new InputStreamReader(
new FileSystemResource(this.outputFile).getInputStream()));
assertThat(results).isEqualTo("item = foo\nitem = bar\n");
});
}
@Test
public void testFormattedFieldExtractorFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(FormattedFieldExtractorJobConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=2",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-8",
"spring.batch.job.flatfilewriter.formatted=true",
"spring.batch.job.flatfilewriter.names=item",
"spring.batch.job.flatfilewriter.format=item = %s");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
AssertFile.assertLineCount(3, this.outputFile);
String results = FileCopyUtils.copyToString(new InputStreamReader(
new FileSystemResource(this.outputFile).getInputStream()));
assertThat(results).isEqualTo("item = f\nitem = b\nitem = b\n");
});
}
@Test
public void testFieldExtractorFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(FieldExtractorConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-8",
"spring.batch.job.flatfilewriter.delimited=true");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
AssertFile.assertLineCount(3, this.outputFile);
String results = FileCopyUtils.copyToString(new InputStreamReader(
new FileSystemResource(this.outputFile).getInputStream()));
assertThat(results).isEqualTo("f\nb\nb\n");
});
}
@Test
public void testCustomLineAggregatorFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(LineAggregatorConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-8");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
AssertFile.assertLineCount(3, this.outputFile);
String results = FileCopyUtils.copyToString(new InputStreamReader(
new FileSystemResource(this.outputFile).getInputStream()));
assertThat(results).isEqualTo("{item=foo}\n{item=bar}\n{item=baz}\n");
});
}
@Test
public void testHeaderFooterFileGeneration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(HeaderFooterConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class,
FlatFileItemWriterAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1", "spring.batch.job.chunkSize=5",
"spring.batch.job.flatfilewriter.name=fooWriter",
String.format(
"spring.batch.job.flatfilewriter.resource=file://%s",
this.outputFile.getAbsolutePath()),
"spring.batch.job.flatfilewriter.encoding=UTF-8",
"spring.batch.job.flatfilewriter.delimited=true",
"spring.batch.job.flatfilewriter.names=item");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
AssertFile.assertLineCount(5, this.outputFile);
String results = FileCopyUtils.copyToString(new InputStreamReader(
new FileSystemResource(this.outputFile).getInputStream()));
assertThat(results).isEqualTo("header\nfoo\nbar\nbaz\nfooter");
});
}
@Configuration
@EnableBatchProcessing
public static class DelimitedJobConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
}
@Configuration
@EnableBatchProcessing
public static class LineAggregatorConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
@Bean
public LineAggregator<Map<Object, Object>> lineAggregator() {
return new PassThroughLineAggregator<>();
}
}
@Configuration
@EnableBatchProcessing
public static class HeaderFooterConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
@Bean
public FlatFileHeaderCallback headerCallback() {
return writer -> writer.append("header");
}
@Bean
public FlatFileFooterCallback footerCallback() {
return writer -> writer.append("footer");
}
}
@Configuration
@EnableBatchProcessing
public static class FieldExtractorConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
@Bean
public FieldExtractor<Map<Object, Object>> lineAggregator() {
return item -> {
List<String> fields = new ArrayList<>(1);
fields.add(((String) item.get("item")).substring(0, 1));
return fields.toArray();
};
}
}
@Configuration
@EnableBatchProcessing
public static class FormattedJobConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "tooLong"));
return new ListItemReader<>(items);
}
}
@Configuration
@EnableBatchProcessing
public static class FormattedFieldExtractorJobConfiguration {
@Bean
public FieldExtractor<Map<Object, Object>> lineAggregator() {
return item -> {
List<String> fields = new ArrayList<>(1);
fields.add(((String) item.get("item")).substring(0, 1));
return fields.toArray();
};
}
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2020-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import org.junit.Test;
import org.springframework.batch.item.file.transform.Range;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Michael Minella
*/
public class RangeConverterTests {
@Test
public void testNullInput() {
RangeConverter converter = new RangeConverter();
assertThat(converter.convert(null)).isNull();
}
@Test
public void testStartValueOnly() {
RangeConverter converter = new RangeConverter();
Range range = converter.convert("5");
assertThat(range.getMin()).isEqualTo(5);
assertThat(range.getMax()).isEqualTo(Integer.MAX_VALUE);
}
@Test
public void testStartAndEndValue() {
RangeConverter converter = new RangeConverter();
Range range = converter.convert("5-25");
assertThat(range.getMin()).isEqualTo(5);
assertThat(range.getMax()).isEqualTo(25);
}
@Test(expected = NumberFormatException.class)
public void testIllegalValue() {
RangeConverter converter = new RangeConverter();
converter.convert("invalid");
}
@Test(expected = IllegalArgumentException.class)
public void testTooManyValues() {
RangeConverter converter = new RangeConverter();
converter.convert("1-2-3-4");
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2020-2020 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
*
* https://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.cloud.task.batch.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.item.support.ListItemWriter;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Michael Minella
*/
public class SingleStepJobAutoConfigurationTests {
@Test
public void testInvalidProperties() {
SingleStepJobProperties properties = new SingleStepJobProperties();
try {
new SingleStepJobAutoConfiguration(null, null, properties, null);
}
catch (IllegalArgumentException iae) {
assertThat(iae.getMessage()).isEqualTo("A job name is required");
}
catch (Throwable t) {
fail("wrong exception was thrown", t);
}
properties.setJobName("job");
try {
new SingleStepJobAutoConfiguration(null, null, properties, null);
}
catch (IllegalArgumentException iae) {
assertThat(iae.getMessage()).isEqualTo("A step name is required");
}
catch (Throwable t) {
fail("wrong exception was thrown", t);
}
properties.setStepName("step");
try {
new SingleStepJobAutoConfiguration(null, null, properties, null);
}
catch (IllegalArgumentException iae) {
assertThat(iae.getMessage()).isEqualTo("A chunk size is required");
}
catch (Throwable t) {
fail("wrong exception was thrown", t);
}
properties.setChunkSize(-5);
try {
new SingleStepJobAutoConfiguration(null, null, properties, null);
}
catch (IllegalArgumentException iae) {
assertThat(iae.getMessage())
.isEqualTo("A chunk size greater than zero is required");
}
catch (Throwable t) {
fail("wrong exception was thrown", t);
}
properties.setChunkSize(5);
}
@Test
public void testSimpleConfiguration() {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withUserConfiguration(SimpleConfiguration.class)
.withConfiguration(
AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
SingleStepJobAutoConfiguration.class))
.withPropertyValues("spring.batch.job.jobName=job",
"spring.batch.job.stepName=step1",
"spring.batch.job.chunkSize=5");
applicationContextRunner.run((context) -> {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
ListItemWriter itemWriter = context.getBean(ListItemWriter.class);
JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
while (jobExplorer.getJobExecution(jobExecution.getJobId()).isRunning()) {
Thread.sleep(1000);
}
List<Map<Object, Object>> writtenItems = itemWriter.getWrittenItems();
assertThat(writtenItems.size()).isEqualTo(3);
assertThat(writtenItems.get(0).get("item")).isEqualTo("foo");
assertThat(writtenItems.get(1).get("item")).isEqualTo("bar");
assertThat(writtenItems.get(2).get("item")).isEqualTo("baz");
});
}
@EnableBatchProcessing
@Configuration
public static class SimpleConfiguration {
@Bean
public ListItemReader<Map<Object, Object>> itemReader() {
List<Map<Object, Object>> items = new ArrayList<>(3);
items.add(Collections.singletonMap("item", "foo"));
items.add(Collections.singletonMap("item", "bar"));
items.add(Collections.singletonMap("item", "baz"));
return new ListItemReader<>(items);
}
@Bean
public ListItemWriter<Map<Object, Object>> itemWriter() {
return new ListItemWriter<>();
}
}
}

View File

@@ -0,0 +1,8 @@
1 2 3 4 5 six
# This should be ignored
7 8 9 1011twelve
$ So should this
1314151617eighteen
1920212223twenty four
1526272829thirty
3132333435thirty six

Binary file not shown.
1 �1�@�2�@�3�@�4�@�5�@�s�i�x�
2 �#� �T�h�i�s� �s�h�o�u�l�d� �b�e� �i�g�n�o�r�e�d�
3 �7�@�8�@�9�@�1�0�@�1�1�@�t�w�e�l�v�e�
4 �$� �S�o� �s�h�o�u�l�d� �t�h�i�s�
5 �1�3�@�1�4�@�1�5�@�1�6�@�1�7�@�e�i�g�h�t�e�e�n�
6 �1�9�@�2�0�@�2�1�@�2�2�@�2�3�@�%�t�w�e�n�t�y� �f�o�u�r�%�
7 �1�5�@�2�6�@�2�7�@�2�8�@�2�9�@�t�h�i�r�t�y�
8 �3�1�@�3�2�@�3�3�@�3�4�@�3�5�@�t�h�i�r�t�y� �s�i�x�
9 �3�7�@�3�8�@�3�9�@�4�0�@�4�1�@�f�o�r�t�y� �t�w�o�
10 �4�3�@�4�4�@�4�5�@�4�6�@�4�7�@�f�o�r�t�y� �e�i�g�h�t�
11 �4�9�@�5�0�@�5�1�@�5�2�@�5�3�@�f�i�f�t�y� �f�o�u�r�
12 �5�5�@�5�6�@�5�7�@�5�8�@�5�9�@�s�i�x�t�y�

View File

@@ -0,0 +1,12 @@
1@2@3@4@5@six
# This should be ignored
7@8@9@10@11@twelve
$ So should this
13@14@15@16@17@eighteen
19@20@21@22@23@%twenty four%
15@26@27@28@29@thirty
31@32@33@34@35@thirty six
37@38@39@40@41@forty two
43@44@45@46@47@forty eight
49@50@51@52@53@fifty four
55@56@57@58@59@sixty
1 1@2@3@4@5@six
2 # This should be ignored
3 7@8@9@10@11@twelve
4 $ So should this
5 13@14@15@16@17@eighteen
6 19@20@21@22@23@%twenty four%
7 15@26@27@28@29@thirty
8 31@32@33@34@35@thirty six
9 37@38@39@40@41@forty two
10 43@44@45@46@47@forty eight
11 49@50@51@52@53@fifty four
12 55@56@57@58@59@sixty