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 ab8601b18c..0ffc4887a6 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; @@ -58,6 +59,7 @@ * @author Glenn Renfro * @author Mahmoud Ben Hassine * @author Drummond Dawson + * @author Daeho Kwon * @since 4.0 * @see FlatFileItemReader */ @@ -87,9 +89,9 @@ public class FlatFileItemReaderBuilder { private LineTokenizer lineTokenizer; - private DelimitedBuilder delimitedBuilder; + private DelimitedSpec delimitedSpec; - private FixedLengthBuilder fixedLengthBuilder; + private FixedLengthSpec fixedLengthSpec; private Class targetType; @@ -304,30 +306,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; } /** @@ -449,11 +477,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."); @@ -519,7 +547,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; @@ -535,7 +563,7 @@ public static class DelimitedBuilder { private boolean strict = true; - protected DelimitedBuilder(FlatFileItemReaderBuilder parent) { + protected DelimitedSpec(FlatFileItemReaderBuilder parent) { this.parent = parent; } @@ -545,7 +573,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; } @@ -556,7 +584,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; } @@ -567,7 +595,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; } @@ -578,7 +606,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; } @@ -592,7 +620,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; } @@ -618,7 +646,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; } @@ -677,7 +705,7 @@ public DelimitedLineTokenizer build() { * * @param the type of the parent {@link FlatFileItemReaderBuilder} */ - public static class FixedLengthBuilder { + public static class FixedLengthSpec { private final FlatFileItemReaderBuilder parent; @@ -689,7 +717,7 @@ public static class FixedLengthBuilder { private FieldSetFactory fieldSetFactory = new DefaultFieldSetFactory(); - protected FixedLengthBuilder(FlatFileItemReaderBuilder parent) { + protected FixedLengthSpec(FlatFileItemReaderBuilder parent) { this.parent = parent; } @@ -699,7 +727,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; } @@ -710,7 +738,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; } @@ -722,7 +750,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; } @@ -745,7 +773,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; } @@ -759,7 +787,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 e6c6f6c2de..fe2bb23107 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. @@ -55,6 +55,7 @@ * @author Mahmoud Ben Hassine * @author Drummond Dawson * @author Glenn Renfro + * @author Daeho Kwon */ class FlatFileItemReaderBuilderTests { @@ -613,6 +614,39 @@ class Person { assertTrue(fieldSetMapper instanceof BeanWrapperFieldSetMapper); } + @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 0b37305559..13971e9763 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. @@ -47,6 +47,7 @@ * @author Mahmoud Ben Hassine * @author Drummond Dawson * @author Glenn Renfro + * @author Daeho Kwon */ class FlatFileItemWriterBuilderTests { @@ -235,6 +236,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 { @@ -289,6 +316,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 {