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:
committed by
Glenn Renfro
parent
8b41515ee3
commit
7e76cb00f3
1
pom.xml
1
pom.xml
@@ -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>
|
||||
|
||||
49
spring-cloud-starter-single-step-batch-job/pom.xml
Normal file
49
spring-cloud-starter-single-step-batch-job/pom.xml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
@@ -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
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user