diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/FlatFileItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/FlatFileItemWriter.java index 19175903c..de46a6930 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/FlatFileItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/FlatFileItemWriter.java @@ -61,11 +61,14 @@ import org.springframework.util.ClassUtils; public class FlatFileItemWriter extends AbstractItemStreamItemWriter implements ResourceAwareItemWriterItemStream, InitializingBean { - private static final boolean DEFAULT_TRANSACTIONAL = true; + public static final boolean DEFAULT_TRANSACTIONAL = true; protected static final Log logger = LogFactory.getLog(FlatFileItemWriter.class); - private static final String DEFAULT_LINE_SEPARATOR = System.getProperty("line.separator"); + public static final String DEFAULT_LINE_SEPARATOR = System.getProperty("line.separator"); + + // default encoding for writing to output files - set to UTF-8. + public static final String DEFAULT_CHARSET = "UTF-8"; private static final String WRITTEN_STATISTICS_NAME = "written"; @@ -85,7 +88,7 @@ InitializingBean { private boolean shouldDeleteIfEmpty = false; - private String encoding = OutputState.DEFAULT_CHARSET; + private String encoding = DEFAULT_CHARSET; private FlatFileHeaderCallback headerCallback; @@ -397,8 +400,6 @@ InitializingBean { * operations on the writer go through this class. */ private class OutputState { - // default encoding for writing to output files - set to UTF-8. - private static final String DEFAULT_CHARSET = "UTF-8"; private FileOutputStream os; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java new file mode 100644 index 000000000..389822ea4 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java @@ -0,0 +1,268 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.item.file.builder; + +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.LineAggregator; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * A builder implementation for the {@link FlatFileItemWriter} + * + * @author Michael Minella + * @since 4.0 + * @see FlatFileItemWriter + */ +public class FlatFileItemWriterBuilder { + + private Resource resource; + + private boolean forceSync = false; + + private String lineSeparator = FlatFileItemWriter.DEFAULT_LINE_SEPARATOR; + + private LineAggregator lineAggregator; + + private String encoding = FlatFileItemWriter.DEFAULT_CHARSET; + + private boolean shouldDeleteIfExists = true; + + private boolean append = false; + + private boolean shouldDeleteIfEmpty = false; + + private boolean saveState = true; + + private FlatFileHeaderCallback headerCallback; + + private FlatFileFooterCallback footerCallback; + + private boolean transactional = FlatFileItemWriter.DEFAULT_TRANSACTIONAL; + + private String name; + + /** + * The name used to calculate the key within the + * {@link org.springframework.batch.item.ExecutionContext}. Required if + * {@link FlatFileItemWriterBuilder#saveState(boolean)} is set to true. + * + * @param name name of the writer instance + * @return The current instance of the builder. + * @see FlatFileItemWriter#setName(String) + */ + public FlatFileItemWriterBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * The {@link Resource} to be used as output. + * + * @param resource the output of the writer. + * @return The current instance of the builder. + * @see FlatFileItemWriter#setResource(Resource) + */ + public FlatFileItemWriterBuilder resource(Resource resource) { + this.resource = resource; + + return this; + } + + /** + * A flag indicating that changes should be force-synced to disk on flush. Defaults + * to false. + * + * @param forceSync value to set the flag to + * @return The current instance of the builder. + * @see FlatFileItemWriter#setForceSync(boolean) + */ + public FlatFileItemWriterBuilder forceSync(boolean forceSync) { + this.forceSync = forceSync; + + return this; + } + + /** + * String used to separate lines in output. Defaults to the System property + * line.separator. + * + * @param lineSeparator value to use for a line separator + * @return The current instance of the builder. + * @see FlatFileItemWriter#setLineSeparator(String) + */ + public FlatFileItemWriterBuilder lineSeparator(String lineSeparator) { + this.lineSeparator = lineSeparator; + + return this; + } + + /** + * Line aggregator used to build the String version of each item. + * + * @param lineAggregator {@link LineAggregator} implementation + * @return The current instance of the builder. + * @see FlatFileItemWriter#setLineAggregator(LineAggregator) + */ + public FlatFileItemWriterBuilder lineAggregator(LineAggregator lineAggregator) { + this.lineAggregator = lineAggregator; + + return this; + } + + /** + * Encoding used for output. + * + * @param encoding encoding type. + * @return The current instance of the builder. + * @see FlatFileItemWriter#setEncoding(String) + */ + public FlatFileItemWriterBuilder encoding(String encoding) { + this.encoding = encoding; + + return this; + } + + /** + * If set to true, once the step is complete, if the resource previously provdied is + * empty, it will be deleted. + * + * @param shouldDelete defaults to false + * @return The current instance of the builder + * @see FlatFileItemWriter#setShouldDeleteIfEmpty(boolean) + */ + public FlatFileItemWriterBuilder shouldDeleteIfEmpty(boolean shouldDelete) { + this.shouldDeleteIfEmpty = shouldDelete; + + return this; + } + + /** + * If set to true, upon the start of the step, if the resource already exists, it will + * be deleted and recreated. + * + * @param shouldDelete defaults to true + * @return The current instance of the builder + * @see FlatFileItemWriter#setShouldDeleteIfExists(boolean) + */ + public FlatFileItemWriterBuilder shouldDeleteIfExists(boolean shouldDelete) { + this.shouldDeleteIfExists = shouldDelete; + + return this; + } + + /** + * If set to true and the file exists, the output will be appended to the existing + * file. + * + * @param append defaults to false + * @return The current instance of the builder + * @see FlatFileItemWriter#setAppendAllowed(boolean) + */ + public FlatFileItemWriterBuilder append(boolean append) { + this.append = append; + + return this; + } + + /** + * If set to false, the state of the output is not maintained and restart is not + * supported. + * + * @param saveState defaults to true + * @return The current instance of the builder + * @see FlatFileItemWriter#setSaveState(boolean) + */ + public FlatFileItemWriterBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * A callback for header processing. + * + * @param callback {@link FlatFileHeaderCallback} impl + * @return The current instance of the builder + * @see FlatFileItemWriter#setHeaderCallback(FlatFileHeaderCallback) + */ + public FlatFileItemWriterBuilder headerCallback(FlatFileHeaderCallback callback) { + this.headerCallback = callback; + + return this; + } + + /** + * A callback for footer processing + * @param callback {@link FlatFileFooterCallback} impl + * @return The current instance of the builder + * @see FlatFileItemWriter#setFooterCallback(FlatFileFooterCallback) + */ + public FlatFileItemWriterBuilder footerCallback(FlatFileFooterCallback callback) { + this.footerCallback = callback; + + return this; + } + + /** + * If set to true, the flushing of the buffer is delayed while a transaction is active. + * + * @param transactional defaults to true + * @return The current instance of the builder + * @see FlatFileItemWriter#setTransactional(boolean) + */ + public FlatFileItemWriterBuilder transactional(boolean transactional) { + this.transactional = transactional; + + return this; + } + + /** + * Validates and builds a {@link FlatFileItemWriter}. + * + * @return a {@link FlatFileItemWriter} + */ + public FlatFileItemWriter build() { + + Assert.notNull(this.lineAggregator, "A LineAggregator is required"); + Assert.notNull(this.resource, "A Resource is required"); + + if(this.saveState) { + Assert.hasText(this.name, "A name is required when saveState is true"); + } + + FlatFileItemWriter writer = new FlatFileItemWriter<>(); + + writer.setName(this.name); + writer.setAppendAllowed(this.append); + writer.setEncoding(this.encoding); + writer.setFooterCallback(this.footerCallback); + writer.setForceSync(this.forceSync); + writer.setHeaderCallback(this.headerCallback); + writer.setLineAggregator(this.lineAggregator); + writer.setLineSeparator(this.lineSeparator); + writer.setResource(this.resource); + writer.setSaveState(this.saveState); + writer.setShouldDeleteIfEmpty(this.shouldDeleteIfEmpty); + writer.setShouldDeleteIfExists(this.shouldDeleteIfExists); + writer.setTransactional(this.transactional); + + return writer; + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilderTests.java new file mode 100644 index 000000000..7d4b17b0f --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilderTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.item.file.builder; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.transform.PassThroughLineAggregator; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Michael Minella + */ +public class FlatFileItemWriterBuilderTests { + + // reads the output file to check the result + private BufferedReader reader; + + @Test(expected = IllegalArgumentException.class) + public void testMissingLineAggregator() { + new FlatFileItemWriterBuilder() + .build(); + } + + @Test + public void test() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .lineAggregator(new PassThroughLineAggregator<>()) + .encoding("UTF-16LE") + .headerCallback(writer1 -> writer1.append("HEADER")) + .footerCallback(writer12 -> writer12.append("FOOTER")) + .build(); + + ExecutionContext executionContext = new ExecutionContext(); + + writer.open(executionContext); + + writer.write(Arrays.asList(new Foo(1, 2, "3"), new Foo(4, 5, "6"))); + + writer.close(); + + assertEquals("HEADER$Foo{first=1, second=2, third='3'}$Foo{first=4, second=5, third='6'}$FOOTER", readLine("UTF-16LE", output)); + } + + @Test + public void testFlags() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .shouldDeleteIfEmpty(true) + .shouldDeleteIfExists(false) + .saveState(false) + .forceSync(true) + .append(true) + .transactional(false) + .lineAggregator(new PassThroughLineAggregator<>()) + .build(); + + assertFalse((Boolean) ReflectionTestUtils.getField(writer, "saveState")); + assertTrue((Boolean) ReflectionTestUtils.getField(writer, "append")); + assertFalse((Boolean) ReflectionTestUtils.getField(writer, "transactional")); + assertTrue((Boolean) ReflectionTestUtils.getField(writer, "shouldDeleteIfEmpty")); + assertFalse((Boolean) ReflectionTestUtils.getField(writer, "shouldDeleteIfExists")); + assertTrue((Boolean) ReflectionTestUtils.getField(writer, "forceSync")); + } + + private String readLine(String encoding, Resource outputFile ) throws IOException { + + if (reader == null) { + reader = new BufferedReader(new InputStreamReader(outputFile.getInputStream(), encoding)); + } + + return reader.readLine(); + } + + public static class Foo { + private int first; + private int second; + private String third; + + public Foo(int first, int second, String third) { + this.first = first; + this.second = second; + this.third = third; + } + + public int getFirst() { + return first; + } + + public void setFirst(int first) { + this.first = first; + } + + public int getSecond() { + return second; + } + + public void setSecond(int second) { + this.second = second; + } + + public String getThird() { + return third; + } + + public void setThird(String third) { + this.third = third; + } + + @Override + public String toString() { + return "Foo{" + + "first=" + first + + ", second=" + second + + ", third='" + third + '\'' + + '}'; + } + } +}