From cc233e6b350ab8894c669bbf01dec39e135e09a2 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Thu, 13 May 2021 09:32:25 -0400 Subject: [PATCH] Add support for quotes in DelimitedLineAggregator Resolves #1139 --- .../builder/FlatFileItemWriterBuilder.java | 17 +++++++++++ .../transform/DelimitedLineAggregator.java | 26 +++++++++++++---- .../FlatFileItemWriterBuilderTests.java | 28 +++++++++++++++++++ .../DelimitedLineAggregatorTests.java | 10 ++++++- 4 files changed, 75 insertions(+), 6 deletions(-) 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 335e06e1cf..291fa2148b 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 @@ -34,6 +34,7 @@ import org.springframework.batch.item.file.transform.RecordFieldExtractor; import org.springframework.core.io.WritableResource; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A builder implementation for the {@link FlatFileItemWriter} @@ -424,6 +425,8 @@ public static class DelimitedBuilder { private String delimiter = ","; + private String quoteCharacter = ""; + private FieldExtractor fieldExtractor; private Class sourceType; @@ -457,6 +460,17 @@ public DelimitedBuilder sourceType(Class sourceType) { return this; } + /** + * Define the quote character for each delimited field. Default is empty string. + * @param quoteCharacter String used as a quote for the aggregate. + * @return The instance of the builder for chaining. + * @see DelimitedLineAggregator#setQuoteCharacter(String) + */ + public DelimitedBuilder quoteCharacter(String quoteCharacter) { + this.quoteCharacter = quoteCharacter; + 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 @@ -489,6 +503,9 @@ public DelimitedLineAggregator build() { if (this.delimiter != null) { delimitedLineAggregator.setDelimiter(this.delimiter); } + if (StringUtils.hasLength(this.quoteCharacter)) { + delimitedLineAggregator.setQuoteCharacter(this.quoteCharacter); + } if (this.fieldExtractor == null) { if (this.sourceType != null && this.sourceType.isRecord()) { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DelimitedLineAggregator.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DelimitedLineAggregator.java index 8e841bed42..a19819b3fe 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DelimitedLineAggregator.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DelimitedLineAggregator.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2007 the original author or authors. + * Copyright 2006-2023 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,19 +15,24 @@ */ package org.springframework.batch.item.file.transform; -import org.springframework.util.StringUtils; +import java.util.Arrays; +import java.util.stream.Collectors; /** * A {@link LineAggregator} implementation that converts an object into a delimited list - * of strings. The default delimiter is a comma. + * of strings. The default delimiter is a comma. An optional quote value can be set to add + * surrounding quotes for each element of the list. Default is empty string, which means + * not quotes. * * @author Dave Syer - * + * @author Glenn Renfro */ public class DelimitedLineAggregator extends ExtractorLineAggregator { private String delimiter = ","; + private String quoteCharacter = ""; + /** * Public setter for the delimiter. * @param delimiter the delimiter to set @@ -36,9 +41,20 @@ public void setDelimiter(String delimiter) { this.delimiter = delimiter; } + /** + * Setter for the quote character. + * @since 5.1 + * @param quoteCharacter the quote character to set + */ + public void setQuoteCharacter(String quoteCharacter) { + this.quoteCharacter = quoteCharacter; + } + @Override public String doAggregate(Object[] fields) { - return StringUtils.arrayToDelimitedString(fields, this.delimiter); + return Arrays.stream(fields) + .map(field -> this.quoteCharacter + field + this.quoteCharacter) + .collect(Collectors.joining(this.delimiter)); } } 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 855876c322..0b37305559 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 @@ -153,6 +153,34 @@ void testDelimitedOutputWithEmptyDelimiter() throws Exception { assertEquals("HEADER$123$456$FOOTER", readLine("UTF-16LE", output)); } + @Test + public void testDelimitedOutputWithEmptyDelimiterAndQuote() throws Exception { + + FileSystemResource output = new FileSystemResource(File.createTempFile("foo", "txt")); + + FlatFileItemWriter writer = new FlatFileItemWriterBuilder().name("foo") + .resource(output) + .lineSeparator("$") + .delimited() + .delimiter("") + .quoteCharacter("%") + .names("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(new Chunk<>(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 void testDelimitedOutputWithDefaultFieldExtractor() throws Exception { diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/DelimitedLineAggregatorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/DelimitedLineAggregatorTests.java index 69c52f6fcd..b592e55554 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/DelimitedLineAggregatorTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/DelimitedLineAggregatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2022 the original author or authors. + * Copyright 2006-2023 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. @@ -22,6 +22,7 @@ /** * @author Dave Syer + * @author Glenn Renfro * */ class DelimitedLineAggregatorTests { @@ -40,6 +41,13 @@ void testSetDelimiter() { assertEquals("foo;bar", aggregator.aggregate(new String[] { "foo", "bar" })); } + @Test + public void testSetDelimiterAndQuote() { + aggregator.setDelimiter(";"); + aggregator.setQuoteCharacter("\""); + assertEquals("\"foo\";\"bar\"", aggregator.aggregate(new String[] { "foo", "bar" })); + } + @Test void testAggregate() { assertEquals("foo,bar", aggregator.aggregate(new String[] { "foo", "bar" }));