diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java index e52d4dbde9..65a17c09a2 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -60,6 +61,7 @@ * @author Drummond Dawson * @author Patrick Baumgartner * @author François Martin + * @author Daeho Kwon * @since 4.0 * @see FlatFileItemReader */ @@ -89,9 +91,9 @@ public class FlatFileItemReaderBuilder { private LineTokenizer lineTokenizer; - private DelimitedBuilder delimitedBuilder; + private DelimitedSpec delimitedSpec; - private FixedLengthBuilder fixedLengthBuilder; + private FixedLengthSpec fixedLengthSpec; private Class targetType; @@ -306,30 +308,56 @@ public FlatFileItemReaderBuilder lineTokenizer(LineTokenizer tokenizer) { } /** - * Returns an instance of a {@link DelimitedBuilder} for building a + * Returns an instance of a {@link DelimitedSpec} for building a * {@link DelimitedLineTokenizer}. The {@link DelimitedLineTokenizer} configured by * this builder will only be used if one is not explicitly configured via * {@link FlatFileItemReaderBuilder#lineTokenizer} - * @return a {@link DelimitedBuilder} + * @return a {@link DelimitedSpec} * */ - public DelimitedBuilder delimited() { - this.delimitedBuilder = new DelimitedBuilder<>(this); - updateTokenizerValidation(this.delimitedBuilder, 1); - return this.delimitedBuilder; + public DelimitedSpec delimited() { + this.delimitedSpec = new DelimitedSpec<>(this); + updateTokenizerValidation(this.delimitedSpec, 1); + return this.delimitedSpec; } /** - * Returns an instance of a {@link FixedLengthBuilder} for building a + * Configure a {@link DelimitedSpec} using a lambda. + * @param consumer the spec to configure + * @return the current builder instance + */ + public FlatFileItemReaderBuilder delimited(Consumer> consumer) { + DelimitedSpec builder = new DelimitedSpec<>(this); + consumer.accept(builder); + this.delimitedSpec = builder; + updateTokenizerValidation(this.delimitedSpec, 1); + return this; + } + + /** + * Returns an instance of a {@link FixedLengthSpec} for building a * {@link FixedLengthTokenizer}. The {@link FixedLengthTokenizer} configured by this * builder will only be used if the {@link FlatFileItemReaderBuilder#lineTokenizer} * has not been configured. - * @return a {@link FixedLengthBuilder} + * @return a {@link FixedLengthSpec} */ - public FixedLengthBuilder fixedLength() { - this.fixedLengthBuilder = new FixedLengthBuilder<>(this); - updateTokenizerValidation(this.fixedLengthBuilder, 2); - return this.fixedLengthBuilder; + public FixedLengthSpec fixedLength() { + this.fixedLengthSpec = new FixedLengthSpec<>(this); + updateTokenizerValidation(this.fixedLengthSpec, 2); + return this.fixedLengthSpec; + } + + /** + * Configure a {@link FixedLengthSpec} using a lambda. + * @param consumer the spec to configure + * @return the current builder instance + */ + public FlatFileItemReaderBuilder fixedLength(Consumer> consumer) { + FixedLengthSpec builder = new FixedLengthSpec<>(this); + consumer.accept(builder); + this.fixedLengthSpec = builder; + updateTokenizerValidation(this.fixedLengthSpec, 2); + return this; } /** @@ -451,11 +479,11 @@ public FlatFileItemReader build() { if (this.lineTokenizer != null) { lineMapper.setLineTokenizer(this.lineTokenizer); } - else if (this.fixedLengthBuilder != null) { - lineMapper.setLineTokenizer(this.fixedLengthBuilder.build()); + else if (this.fixedLengthSpec != null) { + lineMapper.setLineTokenizer(this.fixedLengthSpec.build()); } - else if (this.delimitedBuilder != null) { - lineMapper.setLineTokenizer(this.delimitedBuilder.build()); + else if (this.delimitedSpec != null) { + lineMapper.setLineTokenizer(this.delimitedSpec.build()); } else { throw new IllegalStateException("No LineTokenizer implementation was provided."); @@ -524,7 +552,7 @@ private void updateTokenizerValidation(Object tokenizer, int index) { * * @param the type of the parent {@link FlatFileItemReaderBuilder} */ - public static class DelimitedBuilder { + public static class DelimitedSpec { private final FlatFileItemReaderBuilder parent; @@ -540,7 +568,7 @@ public static class DelimitedBuilder { private boolean strict = true; - protected DelimitedBuilder(FlatFileItemReaderBuilder parent) { + protected DelimitedSpec(FlatFileItemReaderBuilder parent) { this.parent = parent; } @@ -550,7 +578,7 @@ protected DelimitedBuilder(FlatFileItemReaderBuilder parent) { * @return The instance of the builder for chaining. * @see DelimitedLineTokenizer#setDelimiter(String) */ - public DelimitedBuilder delimiter(String delimiter) { + public DelimitedSpec delimiter(String delimiter) { this.delimiter = delimiter; return this; } @@ -561,7 +589,7 @@ public DelimitedBuilder delimiter(String delimiter) { * @return The instance of the builder for chaining. * @see DelimitedLineTokenizer#setQuoteCharacter(char) */ - public DelimitedBuilder quoteCharacter(char quoteCharacter) { + public DelimitedSpec quoteCharacter(char quoteCharacter) { this.quoteCharacter = quoteCharacter; return this; } @@ -572,7 +600,7 @@ public DelimitedBuilder quoteCharacter(char quoteCharacter) { * @return The instance of the builder for chaining. * @see DelimitedLineTokenizer#setIncludedFields(int[]) */ - public DelimitedBuilder includedFields(Integer... fields) { + public DelimitedSpec includedFields(Integer... fields) { this.includedFields.addAll(Arrays.asList(fields)); return this; } @@ -583,7 +611,7 @@ public DelimitedBuilder includedFields(Integer... fields) { * @return The instance of the builder for chaining. * @see DelimitedLineTokenizer#setIncludedFields(int[]) */ - public DelimitedBuilder addIncludedField(int field) { + public DelimitedSpec addIncludedField(int field) { this.includedFields.add(field); return this; } @@ -597,7 +625,7 @@ public DelimitedBuilder addIncludedField(int field) { * @return The instance of the builder for chaining. * @see DelimitedLineTokenizer#setFieldSetFactory(FieldSetFactory) */ - public DelimitedBuilder fieldSetFactory(FieldSetFactory fieldSetFactory) { + public DelimitedSpec fieldSetFactory(FieldSetFactory fieldSetFactory) { this.fieldSetFactory = fieldSetFactory; return this; } @@ -623,7 +651,7 @@ public FlatFileItemReaderBuilder names(String... names) { * @since 5.1 * @param strict the strict flag to set */ - public DelimitedBuilder strict(boolean strict) { + public DelimitedSpec strict(boolean strict) { this.strict = strict; return this; } @@ -682,7 +710,7 @@ public DelimitedLineTokenizer build() { * * @param the type of the parent {@link FlatFileItemReaderBuilder} */ - public static class FixedLengthBuilder { + public static class FixedLengthSpec { private final FlatFileItemReaderBuilder parent; @@ -694,7 +722,7 @@ public static class FixedLengthBuilder { private FieldSetFactory fieldSetFactory = new DefaultFieldSetFactory(); - protected FixedLengthBuilder(FlatFileItemReaderBuilder parent) { + protected FixedLengthSpec(FlatFileItemReaderBuilder parent) { this.parent = parent; } @@ -704,7 +732,7 @@ protected FixedLengthBuilder(FlatFileItemReaderBuilder parent) { * @return This instance for chaining * @see FixedLengthTokenizer#setColumns(Range[]) */ - public FixedLengthBuilder columns(Range... ranges) { + public FixedLengthSpec columns(Range... ranges) { this.ranges.addAll(Arrays.asList(ranges)); return this; } @@ -715,7 +743,7 @@ public FixedLengthBuilder columns(Range... ranges) { * @return This instance for chaining * @see FixedLengthTokenizer#setColumns(Range[]) */ - public FixedLengthBuilder addColumns(Range range) { + public FixedLengthSpec addColumns(Range range) { this.ranges.add(range); return this; } @@ -727,7 +755,7 @@ public FixedLengthBuilder addColumns(Range range) { * @return This instance for chaining * @see FixedLengthTokenizer#setColumns(Range[]) */ - public FixedLengthBuilder addColumns(Range range, int index) { + public FixedLengthSpec addColumns(Range range, int index) { this.ranges.add(index, range); return this; } @@ -750,7 +778,7 @@ public FlatFileItemReaderBuilder names(String... names) { * @return This instance for chaining * @see FixedLengthTokenizer#setStrict(boolean) */ - public FixedLengthBuilder strict(boolean strict) { + public FixedLengthSpec strict(boolean strict) { this.strict = strict; return this; } @@ -764,7 +792,7 @@ public FixedLengthBuilder strict(boolean strict) { * @return The instance of the builder for chaining. * @see FixedLengthTokenizer#setFieldSetFactory(FieldSetFactory) */ - public FixedLengthBuilder fieldSetFactory(FieldSetFactory fieldSetFactory) { + public FixedLengthSpec fieldSetFactory(FieldSetFactory fieldSetFactory) { this.fieldSetFactory = fieldSetFactory; return this; } 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 7de7de5301..6e1954afcc 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 @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -43,6 +44,7 @@ * @author Glenn Renfro * @author Mahmoud Ben Hassine * @author Drummond Dawson + * @author Daeho Kwon * @since 4.0 * @see FlatFileItemWriter */ @@ -76,9 +78,9 @@ public class FlatFileItemWriterBuilder { private String name; - private DelimitedBuilder delimitedBuilder; + private DelimitedSpec delimitedSpec; - private FormattedBuilder formattedBuilder; + private FormattedSpec formattedSpec; /** * Configure if the state of the @@ -246,29 +248,53 @@ public FlatFileItemWriterBuilder transactional(boolean transactional) { } /** - * Returns an instance of a {@link DelimitedBuilder} for building a + * Returns an instance of a {@link DelimitedSpec} 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} + * @return a {@link DelimitedSpec} * */ - public DelimitedBuilder delimited() { - this.delimitedBuilder = new DelimitedBuilder<>(this); - return this.delimitedBuilder; + public DelimitedSpec delimited() { + this.delimitedSpec = new DelimitedSpec<>(this); + return this.delimitedSpec; } /** - * Returns an instance of a {@link FormattedBuilder} for building a + * Configure a {@link DelimitedSpec} using a lambda. + * @param consumer the spec to configure + * @return the current builder instance + */ + public FlatFileItemWriterBuilder delimited(Consumer> consumer) { + DelimitedSpec builder = new DelimitedSpec<>(this); + consumer.accept(builder); + this.delimitedSpec = builder; + return this; + } + + /** + * Returns an instance of a {@link FormattedSpec} 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} + * @return a {@link FormattedSpec} * */ - public FormattedBuilder formatted() { - this.formattedBuilder = new FormattedBuilder<>(this); - return this.formattedBuilder; + public FormattedSpec formatted() { + this.formattedSpec = new FormattedSpec<>(this); + return this.formattedSpec; + } + + /** + * Configure a {@link FormattedSpec} using a lambda. + * @param consumer the spec to configure + * @return the current builder instance + */ + public FlatFileItemWriterBuilder formatted(Consumer> consumer) { + FormattedSpec builder = new FormattedSpec<>(this); + consumer.accept(builder); + this.formattedSpec = builder; + return this; } /** @@ -276,7 +302,7 @@ public FormattedBuilder formatted() { * * @param the type of the parent {@link FlatFileItemWriterBuilder} */ - public static class FormattedBuilder { + public static class FormattedSpec { private final FlatFileItemWriterBuilder parent; @@ -294,7 +320,7 @@ public static class FormattedBuilder { private Class sourceType; - protected FormattedBuilder(FlatFileItemWriterBuilder parent) { + protected FormattedSpec(FlatFileItemWriterBuilder parent) { this.parent = parent; } @@ -303,7 +329,7 @@ protected FormattedBuilder(FlatFileItemWriterBuilder parent) { * @param format used to aggregate items * @return The instance of the builder for chaining. */ - public FormattedBuilder format(String format) { + public FormattedSpec format(String format) { this.format = format; return this; } @@ -313,7 +339,7 @@ public FormattedBuilder format(String format) { * @param locale to use * @return The instance of the builder for chaining. */ - public FormattedBuilder locale(Locale locale) { + public FormattedSpec locale(Locale locale) { this.locale = locale; return this; } @@ -324,7 +350,7 @@ public FormattedBuilder locale(Locale locale) { * @param minimumLength of the formatted string * @return The instance of the builder for chaining. */ - public FormattedBuilder minimumLength(int minimumLength) { + public FormattedSpec minimumLength(int minimumLength) { this.minimumLength = minimumLength; return this; } @@ -335,7 +361,7 @@ public FormattedBuilder minimumLength(int minimumLength) { * @param maximumLength of the formatted string * @return The instance of the builder for chaining. */ - public FormattedBuilder maximumLength(int maximumLength) { + public FormattedSpec maximumLength(int maximumLength) { this.maximumLength = maximumLength; return this; } @@ -348,7 +374,7 @@ public FormattedBuilder maximumLength(int maximumLength) { * @return The current instance of the builder. * @since 5.0 */ - public FormattedBuilder sourceType(Class sourceType) { + public FormattedSpec sourceType(Class sourceType) { this.sourceType = sourceType; return this; @@ -368,7 +394,7 @@ public FlatFileItemWriterBuilder fieldExtractor(FieldExtractor fieldExtrac * 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)}. + * via {@link FormattedSpec#fieldExtractor(FieldExtractor)}. * @param names names of each field * @return The parent {@link FlatFileItemWriterBuilder} * @see BeanWrapperFieldExtractor#setNames(String[]) @@ -417,7 +443,7 @@ public FormatterLineAggregator build() { * * @param the type of the parent {@link FlatFileItemWriterBuilder} */ - public static class DelimitedBuilder { + public static class DelimitedSpec { private final FlatFileItemWriterBuilder parent; @@ -431,7 +457,7 @@ public static class DelimitedBuilder { private Class sourceType; - protected DelimitedBuilder(FlatFileItemWriterBuilder parent) { + protected DelimitedSpec(FlatFileItemWriterBuilder parent) { this.parent = parent; } @@ -441,7 +467,7 @@ protected DelimitedBuilder(FlatFileItemWriterBuilder parent) { * @return The instance of the builder for chaining. * @see DelimitedLineAggregator#setDelimiter(String) */ - public DelimitedBuilder delimiter(String delimiter) { + public DelimitedSpec delimiter(String delimiter) { this.delimiter = delimiter; return this; } @@ -454,7 +480,7 @@ public DelimitedBuilder delimiter(String delimiter) { * @return The current instance of the builder. * @since 5.0 */ - public DelimitedBuilder sourceType(Class sourceType) { + public DelimitedSpec sourceType(Class sourceType) { this.sourceType = sourceType; return this; @@ -467,7 +493,7 @@ public DelimitedBuilder sourceType(Class sourceType) { * @see DelimitedLineAggregator#setQuoteCharacter(String) * @since 5.1 */ - public DelimitedBuilder quoteCharacter(String quoteCharacter) { + public DelimitedSpec quoteCharacter(String quoteCharacter) { this.quoteCharacter = quoteCharacter; return this; } @@ -476,7 +502,7 @@ public DelimitedBuilder quoteCharacter(String quoteCharacter) { * 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)}. + * via {@link DelimitedSpec#fieldExtractor(FieldExtractor)}. * @param names names of each field * @return The parent {@link FlatFileItemWriterBuilder} * @see BeanWrapperFieldExtractor#setNames(String[]) @@ -537,8 +563,8 @@ public DelimitedLineAggregator build() { */ public FlatFileItemWriter build() { - Assert.isTrue(this.lineAggregator != null || this.delimitedBuilder != null || this.formattedBuilder != null, - "A LineAggregator or a DelimitedBuilder or a FormattedBuilder is required"); + Assert.isTrue(this.lineAggregator != null || this.delimitedSpec != null || this.formattedSpec != null, + "A LineAggregator or a DelimitedSpec or a FormattedSpec is required"); if (this.saveState) { Assert.hasText(this.name, "A name is required when saveState is true"); @@ -558,13 +584,13 @@ public FlatFileItemWriter build() { writer.setForceSync(this.forceSync); writer.setHeaderCallback(this.headerCallback); if (this.lineAggregator == null) { - Assert.state(this.delimitedBuilder == null || this.formattedBuilder == null, + Assert.state(this.delimitedSpec == null || this.formattedSpec == null, "Either a DelimitedLineAggregator or a FormatterLineAggregator should be provided, but not both"); - if (this.delimitedBuilder != null) { - this.lineAggregator = this.delimitedBuilder.build(); + if (this.delimitedSpec != null) { + this.lineAggregator = this.delimitedSpec.build(); } else { - this.lineAggregator = this.formattedBuilder.build(); + this.lineAggregator = this.formattedSpec.build(); } } writer.setLineAggregator(this.lineAggregator); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java index 3e824f4a96..194ddd2161 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -57,8 +57,10 @@ * @author Mahmoud Ben Hassine * @author Drummond Dawson * @author Glenn Renfro + * @author Daeho Kwon * @author Patrick Baumgartner * @author François Martin + * @author Daeho Kwon */ class FlatFileItemReaderBuilderTests { @@ -628,6 +630,39 @@ class Person { assertInstanceOf(BeanWrapperFieldSetMapper.class, fieldSetMapper); } + @Test + void testDelimitedBuilderLambda() throws Exception { + FlatFileItemReader reader = new FlatFileItemReaderBuilder().name("fooReader") + .resource(getResource("1,2,3")) + .delimited(config -> config.delimiter(",").names("first", "second", "third")) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + assertNull(reader.read()); + } + + @Test + void testFixedLengthBuilderLambda() throws Exception { + FlatFileItemReader reader = new FlatFileItemReaderBuilder().name("fooReader") + .resource(getResource("1 2 3")) + .fixedLength(config -> config.columns(new Range(1, 3), new Range(4, 6), new Range(7)) + .names("first", "second", "third")) + .targetType(Foo.class) + .build(); + + reader.open(new ExecutionContext()); + Foo item = reader.read(); + assertEquals(1, item.getFirst()); + assertEquals(2, item.getSecond()); + assertEquals("3", item.getThird()); + assertNull(reader.read()); + } + private Resource getResource(String contents) { return new ByteArrayResource(contents.getBytes()); } 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 ad8083c4d2..63ca71b4ec 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-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -48,6 +48,7 @@ * @author Mahmoud Ben Hassine * @author Drummond Dawson * @author Glenn Renfro + * @author Daeho Kwon */ class FlatFileItemWriterBuilderTests { @@ -236,6 +237,32 @@ void testDelimitedOutputWithCustomFieldExtractor() throws Exception { assertEquals("HEADER$1 3$4 6$FOOTER", readLine("UTF-16LE", output)); } + @Test + void testDelimitedWithLambda() throws Exception { + + WritableResource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder().name("foo") + .resource(output) + .lineSeparator("$") + .delimited(config -> config.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(Chunk.of(new Foo(1, 2, "3"), new Foo(4, 5, "6"))); + + writer.close(); + + assertEquals("HEADER$1 3$4 6$FOOTER", readLine("UTF-16LE", output)); + } + @Test void testFormattedOutputWithDefaultFieldExtractor() throws Exception { @@ -290,6 +317,32 @@ void testFormattedOutputWithCustomFieldExtractor() throws Exception { assertEquals("HEADER$ 1 3$ 4 6$FOOTER", readLine("UTF-16LE", output)); } + @Test + void testFormattedWithLambda() throws Exception { + + WritableResource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder().name("foo") + .resource(output) + .lineSeparator("$") + .formatted(config -> config.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(Chunk.of(new Foo(1, 2, "3"), new Foo(4, 5, "6"))); + + writer.close(); + + assertEquals("HEADER$ 1 3$ 4 6$FOOTER", readLine("UTF-16LE", output)); + } + @Test void testFlags() throws Exception {