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 index 574fa7b9d5..9ce46eb0d4 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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. @@ -15,18 +15,29 @@ */ package org.springframework.batch.item.file.builder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + 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.BeanWrapperFieldExtractor; +import org.springframework.batch.item.file.transform.DelimitedLineAggregator; +import org.springframework.batch.item.file.transform.FieldExtractor; +import org.springframework.batch.item.file.transform.FormatterLineAggregator; import org.springframework.batch.item.file.transform.LineAggregator; import org.springframework.core.io.Resource; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A builder implementation for the {@link FlatFileItemWriter} * * @author Michael Minella * @author Glenn Renfro + * @author Mahmoud Ben Hassine * @since 4.0 * @see FlatFileItemWriter */ @@ -58,6 +69,10 @@ public class FlatFileItemWriterBuilder { private String name; + private DelimitedBuilder delimitedBuilder; + + private FormattedBuilder formattedBuilder; + /** * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport} * should be persisted within the {@link org.springframework.batch.item.ExecutionContext} @@ -155,7 +170,7 @@ public FlatFileItemWriterBuilder encoding(String encoding) { } /** - * If set to true, once the step is complete, if the resource previously provdied is + * If set to true, once the step is complete, if the resource previously provided is * empty, it will be deleted. * * @param shouldDelete defaults to false @@ -234,6 +249,236 @@ public FlatFileItemWriterBuilder transactional(boolean transactional) { return this; } + /** + * Returns an instance of a {@link DelimitedBuilder} for building a + * {@link DelimitedLineAggregator}. The {@link DelimitedLineAggregator} configured by + * this builder will only be used if one is not explicitly configured via + * {@link FlatFileItemWriterBuilder#lineAggregator} + * + * @return a {@link DelimitedBuilder} + * + */ + public DelimitedBuilder delimited() { + this.delimitedBuilder = new DelimitedBuilder<>(this); + return this.delimitedBuilder; + } + + /** + * Returns an instance of a {@link FormattedBuilder} for building a + * {@link FormatterLineAggregator}. The {@link FormatterLineAggregator} configured by + * this builder will only be used if one is not explicitly configured via + * {@link FlatFileItemWriterBuilder#lineAggregator} + * + * @return a {@link FormattedBuilder} + * + */ + public FormattedBuilder formatted() { + this.formattedBuilder = new FormattedBuilder<>(this); + return this.formattedBuilder; + } + + /** + * A builder for constructing a {@link FormatterLineAggregator}. + * + * @param the type of the parent {@link FlatFileItemWriterBuilder} + */ + public static class FormattedBuilder { + + private FlatFileItemWriterBuilder parent; + + private String format; + + private Locale locale = Locale.getDefault(); + + private int maximumLength = 0; + + private int minimumLength = 0; + + private FieldExtractor fieldExtractor; + + private List names = new ArrayList<>(); + + protected FormattedBuilder(FlatFileItemWriterBuilder parent) { + this.parent = parent; + } + + /** + * Set the format string used to aggregate items + * @param format used to aggregate items + * @return The instance of the builder for chaining. + */ + public FormattedBuilder format(String format) { + this.format = format; + return this; + } + + /** + * Set the locale. + * @param locale to use + * @return The instance of the builder for chaining. + */ + public FormattedBuilder locale(Locale locale) { + this.locale = locale; + return this; + } + + /** + * Set the minimum length of the formatted string. If this is not set + * the default is to allow any length. + * @param minimumLength of the formatted string + * @return The instance of the builder for chaining. + */ + public FormattedBuilder minimumLength(int minimumLength) { + this.minimumLength = minimumLength; + return this; + } + + /** + * Set the maximum length of the formatted string. If this is not set + * the default is to allow any length. + * @param maximumLength of the formatted string + * @return The instance of the builder for chaining. + */ + public FormattedBuilder maximumLength(int maximumLength) { + this.maximumLength = maximumLength; + return this; + } + + /** + * Set the {@link FieldExtractor} to use to extract fields from each item. + * @param fieldExtractor to use to extract fields from each item + * @return The current instance of the builder + */ + public FlatFileItemWriterBuilder fieldExtractor(FieldExtractor fieldExtractor) { + this.fieldExtractor = fieldExtractor; + return this.parent; + } + + /** + * Names of each of the fields within the fields that are returned in the order + * they occur within the formatted file. These names will be used to create + * a {@link BeanWrapperFieldExtractor} only if no explicit field extractor + * is set via {@link FormattedBuilder#fieldExtractor(FieldExtractor)}. + * + * @param names names of each field + * @return The parent {@link FlatFileItemWriterBuilder} + * @see BeanWrapperFieldExtractor#setNames(String[]) + */ + public FlatFileItemWriterBuilder names(String[] names) { + this.names.addAll(Arrays.asList(names)); + return this.parent; + } + + public FormatterLineAggregator build() { + Assert.notNull(this.format, "A format is required"); + Assert.isTrue((this.names != null && !this.names.isEmpty()) || this.fieldExtractor != null, + "A list of field names or a field extractor is required"); + + FormatterLineAggregator formatterLineAggregator = new FormatterLineAggregator<>(); + formatterLineAggregator.setFormat(this.format); + formatterLineAggregator.setLocale(this.locale); + formatterLineAggregator.setMinimumLength(this.minimumLength); + formatterLineAggregator.setMaximumLength(this.maximumLength); + + if (this.fieldExtractor == null) { + BeanWrapperFieldExtractor beanWrapperFieldExtractor = new BeanWrapperFieldExtractor<>(); + beanWrapperFieldExtractor.setNames(this.names.toArray(new String[this.names.size()])); + try { + beanWrapperFieldExtractor.afterPropertiesSet(); + } + catch (Exception e) { + throw new IllegalStateException("Unable to initialize FormatterLineAggregator", e); + } + this.fieldExtractor = beanWrapperFieldExtractor; + } + + formatterLineAggregator.setFieldExtractor(this.fieldExtractor); + return formatterLineAggregator; + } + } + + /** + * A builder for constructing a {@link DelimitedLineAggregator} + * + * @param the type of the parent {@link FlatFileItemWriterBuilder} + */ + public static class DelimitedBuilder { + + private FlatFileItemWriterBuilder parent; + + private List names = new ArrayList<>(); + + private String delimiter = ","; + + private FieldExtractor fieldExtractor; + + protected DelimitedBuilder(FlatFileItemWriterBuilder parent) { + this.parent = parent; + } + + /** + * Define the delimiter for the file. + * + * @param delimiter String used as a delimiter between fields. + * @return The instance of the builder for chaining. + * @see DelimitedLineAggregator#setDelimiter(String) + */ + public DelimitedBuilder delimiter(String delimiter) { + this.delimiter = delimiter; + return this; + } + + /** + * Names of each of the fields within the fields that are returned in the order + * they occur within the delimited file. These names will be used to create + * a {@link BeanWrapperFieldExtractor} only if no explicit field extractor + * is set via {@link DelimitedBuilder#fieldExtractor(FieldExtractor)}. + * + * @param names names of each field + * @return The parent {@link FlatFileItemWriterBuilder} + * @see BeanWrapperFieldExtractor#setNames(String[]) + */ + public FlatFileItemWriterBuilder names(String[] names) { + this.names.addAll(Arrays.asList(names)); + return this.parent; + } + + /** + * Set the {@link FieldExtractor} to use to extract fields from each item. + * @param fieldExtractor to use to extract fields from each item + * @return The parent {@link FlatFileItemWriterBuilder} + */ + public FlatFileItemWriterBuilder fieldExtractor(FieldExtractor fieldExtractor) { + this.fieldExtractor = fieldExtractor; + return this.parent; + } + + public DelimitedLineAggregator build() { + Assert.isTrue((this.names != null && !this.names.isEmpty()) || this.fieldExtractor != null, + "A list of field names or a field extractor is required"); + + DelimitedLineAggregator delimitedLineAggregator = new DelimitedLineAggregator<>(); + if (StringUtils.hasLength(this.delimiter)) { + delimitedLineAggregator.setDelimiter(this.delimiter); + } + + if (this.fieldExtractor == null) { + BeanWrapperFieldExtractor beanWrapperFieldExtractor = new BeanWrapperFieldExtractor<>(); + beanWrapperFieldExtractor.setNames(this.names.toArray(new String[this.names.size()])); + try { + beanWrapperFieldExtractor.afterPropertiesSet(); + } + catch (Exception e) { + throw new IllegalStateException("Unable to initialize DelimitedLineAggregator", e); + } + this.fieldExtractor = beanWrapperFieldExtractor; + } + + delimitedLineAggregator.setFieldExtractor(this.fieldExtractor); + return delimitedLineAggregator; + } + } + /** * Validates and builds a {@link FlatFileItemWriter}. * @@ -241,7 +486,8 @@ public FlatFileItemWriterBuilder transactional(boolean transactional) { */ public FlatFileItemWriter build() { - Assert.notNull(this.lineAggregator, "A LineAggregator is required"); + Assert.isTrue(this.lineAggregator != null || this.delimitedBuilder != null || this.formattedBuilder != null, + "A LineAggregator or a DelimitedBuilder or a FormattedBuilder is required"); Assert.notNull(this.resource, "A Resource is required"); if(this.saveState) { @@ -256,6 +502,16 @@ public FlatFileItemWriter build() { writer.setFooterCallback(this.footerCallback); writer.setForceSync(this.forceSync); writer.setHeaderCallback(this.headerCallback); + if (this.lineAggregator == null) { + Assert.state(this.delimitedBuilder == null || this.formattedBuilder == null, + "Either a DelimitedLineAggregator or a FormatterLineAggregator should be provided, but not both"); + if (this.delimitedBuilder != null) { + this.lineAggregator = this.delimitedBuilder.build(); + } + else { + this.lineAggregator = this.formattedBuilder.build(); + } + } writer.setLineAggregator(this.lineAggregator); writer.setLineSeparator(this.lineSeparator); writer.setResource(this.resource); 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 index 7d4b17b0fa..0b9e04e352 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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. @@ -36,6 +36,7 @@ /** * @author Michael Minella + * @author Mahmoud Ben Hassine */ public class FlatFileItemWriterBuilderTests { @@ -48,6 +49,22 @@ public void testMissingLineAggregator() { .build(); } + @Test(expected = IllegalStateException.class) + public void testMultipleLineAggregators() throws IOException { + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + new FlatFileItemWriterBuilder() + .name("itemWriter") + .resource(output) + .delimited() + .delimiter(";") + .names(new String[] {"foo", "bar"}) + .formatted() + .format("%2s%2s") + .names(new String[] {"foo", "bar"}) + .build(); + } + @Test public void test() throws Exception { @@ -74,6 +91,145 @@ public void test() throws Exception { assertEquals("HEADER$Foo{first=1, second=2, third='3'}$Foo{first=4, second=5, third='6'}$FOOTER", readLine("UTF-16LE", output)); } + @Test + public void testDelimitedOutputWithDefaultDelimiter() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .delimited() + .names(new String[] {"first", "second", "third"}) + .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$1,2,3$4,5,6$FOOTER", readLine("UTF-16LE", output)); + } + + @Test + public void testDelimitedOutputWithDefaultFieldExtractor() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .delimited() + .delimiter(";") + .names(new String[] {"first", "second", "third"}) + .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$1;2;3$4;5;6$FOOTER", readLine("UTF-16LE", output)); + } + + @Test + public void testDelimitedOutputWithCustomFieldExtractor() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .delimited() + .delimiter(" ") + .fieldExtractor(item -> new Object[] {item.getFirst(), item.getThird()}) + .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$1 3$4 6$FOOTER", readLine("UTF-16LE", output)); + } + + @Test + public void testFormattedOutputWithDefaultFieldExtractor() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .formatted() + .format("%2s%2s%2s") + .names(new String[] {"first", "second", "third"}) + .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$ 1 2 3$ 4 5 6$FOOTER", readLine("UTF-16LE", output)); + } + + @Test + public void testFormattedOutputWithCustomFieldExtractor() throws Exception { + + Resource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder() + .name("foo") + .resource(output) + .lineSeparator("$") + .formatted() + .format("%3s%3s") + .fieldExtractor(item -> new Object[] {item.getFirst(), item.getThird()}) + .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$ 1 3$ 4 6$FOOTER", readLine("UTF-16LE", output)); + } + @Test public void testFlags() throws Exception {