From a5549ddcdb374bf13b11fff4b6986b086de0469f Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Fri, 19 May 2023 15:39:23 +0200 Subject: [PATCH 1/7] Add CsvEncoder (#483) Copied from https://github.com/metafacture/metafacture-csv-plugin. Original author: eberhardtj (j.eberhardt@dnb.de). --- .../java/org/metafacture/csv/CsvEncoder.java | 226 ++++++++++++++++++ .../main/resources/flux-commands.properties | 1 + .../org/metafacture/csv/CsvEncoderTest.java | 180 ++++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java create mode 100644 metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java diff --git a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java new file mode 100644 index 000000000..d2ad6c31b --- /dev/null +++ b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java @@ -0,0 +1,226 @@ +/* + * Copyright 2018-2023 Deutsche Nationalbibliothek et al + * + * 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.metafacture.csv; + +import org.metafacture.framework.FluxCommand; +import org.metafacture.framework.MetafactureException; +import org.metafacture.framework.ObjectReceiver; +import org.metafacture.framework.StreamReceiver; +import org.metafacture.framework.annotations.Description; +import org.metafacture.framework.annotations.In; +import org.metafacture.framework.annotations.Out; +import org.metafacture.framework.helpers.DefaultStreamPipe; + +import com.opencsv.CSVWriter; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A csv encoder that converts a record into a csv line (Default separator: {@value #DEFAULT_SEP}). + * + *

+ * Each record represents a row. Each literal value represents a column value. + *

+ * + * @author eberhardtj (j.eberhardt@dnb.de) + */ +@Description("Encodes each value in a record as a csv row.") +@In(StreamReceiver.class) +@Out(String.class) +@FluxCommand("encode-csv") +public class CsvEncoder extends DefaultStreamPipe> { + public static final char DEFAULT_SEP = CSVWriter.DEFAULT_SEPARATOR; + private CSVWriter csvWriter; + private StringWriter writer; + private List rowItems = new ArrayList<>(); + private boolean isFirstRecord = true; + private List header = new ArrayList<>(); + private char separator = DEFAULT_SEP; + private boolean noQuotes; + private boolean includeHeader; + private boolean includeRecordId; + + /** + * Creates an instance of {@link CsvEncoder} with a given separator. + * + * @param separator to separate columns + */ + public CsvEncoder(final String separator) { + this.separator = separator.charAt(0); + } + + /** + * Creates an instance of {@link CsvEncoder} with a given separator. + * + * @param separator to separate columns + */ + public CsvEncoder(final char separator) { + this.separator = separator; + } + + /** + * Creates an instance of {@link CsvEncoder}. The default separator is + * {@value #DEFAULT_SEP}. + */ + public CsvEncoder() { + } + + /** + * Start each line with the record ID. + * Default is to not start each line with the record ID. + * + * @param includeRecordId true if the first column should consist of the record's ID + */ + public void setIncludeRecordId(final boolean includeRecordId) { + this.includeRecordId = includeRecordId; + } + + /** + * Add first record as a column description header. + * Default is to not add a column description. + * + * @param includeHeader true if the first record should act as a CSV header, otherwise false + */ + public void setIncludeHeader(final boolean includeHeader) { + this.includeHeader = includeHeader; + } + + /** + * Add a character to separate the columns. + * The default is {@value #DEFAULT_SEP}. + * + * @param separator set the character which separates the columns + */ + public void setSeparator(final String separator) { + if (separator.length() > 1) { + throw new MetafactureException("Separator needs to be a single character."); + } + this.separator = separator.charAt(0); + } + + /** + * Add a character to separate the columns. + * The default is {@value #DEFAULT_SEP}. + * + * @param separator set the character which separates the columns + */ + public void setSeparator(final char separator) { + this.separator = separator; + } + + /** + * Set if values should be not quoted by '"'. + * The default is to quote values. + * + * @param noQuotes true if no quotes should be used. Default is false. + */ + public void setNoQuotes(final boolean noQuotes) { + this.noQuotes = noQuotes; + } + + private void initialize() { + writer = new StringWriter(); + final String emptyLineEnd = ""; + csvWriter = new CSVWriter(writer, + separator, + noQuotes ? CSVWriter.NO_QUOTE_CHARACTER : CSVWriter.DEFAULT_QUOTE_CHARACTER, + CSVWriter.DEFAULT_ESCAPE_CHARACTER, + emptyLineEnd); + } + + private String[] arrayOf(final List list) { + final int length = list.size(); + return list.toArray(new String[length]); + } + + private void resetCaches() { + this.rowItems = new ArrayList<>(); + } + + private void writeRow(final List rowItemsArray) { + final String[] row = arrayOf(rowItemsArray); + csvWriter.writeNext(row); + final String line = writer.toString(); + getReceiver().process(line); + writer.getBuffer().setLength(0); + } + + @Override + public void startRecord(final String identifier) { + if (isFirstRecord) { + initialize(); + if (includeRecordId) { + header.add("record id"); + } + } + + rowItems = new ArrayList<>(); + + if (includeRecordId) { + rowItems.add(identifier); + } + } + + @Override + public void endRecord() { + if (isFirstRecord) { + if (includeHeader) { + final List uniqueHeader = header.stream().distinct().collect(Collectors.toList()); + writeRow(uniqueHeader); + header.clear(); + } + isFirstRecord = false; + } + + writeRow(rowItems); + + resetCaches(); + } + + @Override + public void literal(final String name, final String value) { + if (isFirstRecord) { + header.add(name); + } + rowItems.add(value); + } + + @Override + public void onCloseStream() { + try { + csvWriter.close(); + } + catch (final IOException e) { + throw new MetafactureException(e); + } + } + + @Override + public void onResetStream() { + this.includeRecordId = false; + this.includeHeader = false; + this.header = new ArrayList<>(); + + this.isFirstRecord = true; + this.rowItems = new ArrayList<>(); + } + +} diff --git a/metafacture-csv/src/main/resources/flux-commands.properties b/metafacture-csv/src/main/resources/flux-commands.properties index d51970343..8b55b5c67 100644 --- a/metafacture-csv/src/main/resources/flux-commands.properties +++ b/metafacture-csv/src/main/resources/flux-commands.properties @@ -14,3 +14,4 @@ # limitations under the License. # decode-csv org.metafacture.csv.CsvDecoder +encode-csv org.metafacture.csv.CsvEncoder diff --git a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java new file mode 100644 index 000000000..e36e07006 --- /dev/null +++ b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2018-2023 Deutsche Nationalbibliothek et al + * + * 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.metafacture.csv; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.metafacture.framework.ObjectReceiver; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Mockito.inOrder; + +/** + * Tests for {@link CsvEncoder}. + * + * @author eberhardtj (j.eberhardt@dnb.de) + * @author Pascal Christoph (dr0i) + */ +public final class CsvEncoderTest { + + private CsvEncoder encoder; + + @Mock + private ObjectReceiver receiver; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + encoder = new CsvEncoder(); + encoder.setIncludeHeader(false); + encoder.setReceiver(receiver); + } + + @After + public void cleanup() { + encoder.closeStream(); + } + + @Test + public void shouldReceiveSingleRecord() { + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"a\",\"b\""); + } + + @Test + public void shouldHaveNoQuotes() { + encoder.setNoQuotes(true); + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("a,b"); + } + + @Test + public void shouldReceiveSingleRecordWithHeader() { + encoder.setIncludeHeader(true); + + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"column 1\",\"column 2\""); + ordered.verify(receiver).process("\"a\",\"b\""); + } + + @Test + public void shouldReceiveSingleRecordWithRecordId() { + encoder.setIncludeRecordId(true); + + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"1\",\"a\",\"b\""); + } + + @Test + public void shouldReceiveSingleRecordWithRecordIdAndHeader() { + encoder.setIncludeRecordId(true); + encoder.setIncludeHeader(true); + + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"record id\",\"column 1\",\"column 2\""); + ordered.verify(receiver).process("\"1\",\"a\",\"b\""); + } + + @Test + public void shouldReceiveThreeRows() { + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.startRecord("2"); + encoder.literal("column 1", "c"); + encoder.literal("column 2", "d"); + encoder.endRecord(); + encoder.startRecord("3"); + encoder.literal("column 1", "e"); + encoder.literal("column 2", "f"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"a\",\"b\""); + ordered.verify(receiver).process("\"c\",\"d\""); + ordered.verify(receiver).process("\"e\",\"f\""); + } + + @Test + public void shouldUseTabulatorAsSeparator() { + encoder.setSeparator('\t'); + + encoder.startRecord("1"); + encoder.literal("column 1", "a"); + encoder.literal("column 2", "b"); + encoder.endRecord(); + encoder.startRecord("2"); + encoder.literal("column 1", "c"); + encoder.literal("column 2", "d"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"a\"\t\"b\""); + ordered.verify(receiver).process("\"c\"\t\"d\""); + } + + @Test + public void shouldNotCreateNestedCsvInColumn() { + encoder.startRecord("1"); + encoder.literal("name", "a"); + encoder.literal("alias", "a1"); + encoder.literal("alias", "a2"); + encoder.literal("alias", "a3"); + encoder.endRecord(); + encoder.closeStream(); + + final InOrder ordered = inOrder(receiver); + ordered.verify(receiver).process("\"a\",\"a1\",\"a2\",\"a3\""); + } + +} From e48a0e115db61978f76a27a876c32afe1333a6ae Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Fri, 19 May 2023 18:10:37 +0200 Subject: [PATCH 2/7] Update metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java Co-authored-by: Jens Wille --- .../src/main/java/org/metafacture/csv/CsvEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java index d2ad6c31b..8236d603f 100644 --- a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java +++ b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java @@ -104,7 +104,7 @@ public void setIncludeHeader(final boolean includeHeader) { } /** - * Add a character to separate the columns. + * Set the character to separate the columns. * The default is {@value #DEFAULT_SEP}. * * @param separator set the character which separates the columns From 8759ed6a5d756c9c9dfc53d1329c19252ab68ff2 Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Fri, 19 May 2023 18:10:46 +0200 Subject: [PATCH 3/7] Update metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java Co-authored-by: Jens Wille --- .../src/main/java/org/metafacture/csv/CsvEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java index 8236d603f..f2f91db49 100644 --- a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java +++ b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java @@ -117,7 +117,7 @@ public void setSeparator(final String separator) { } /** - * Add a character to separate the columns. + * Set the character to separate the columns. * The default is {@value #DEFAULT_SEP}. * * @param separator set the character which separates the columns From 96811dad502d6c4b0b2439f108cfcc25a56b5928 Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Fri, 19 May 2023 18:11:04 +0200 Subject: [PATCH 4/7] Update metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java Co-authored-by: Jens Wille --- .../src/main/java/org/metafacture/csv/CsvEncoder.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java index f2f91db49..513884dd0 100644 --- a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java +++ b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java @@ -183,8 +183,7 @@ public void startRecord(final String identifier) { public void endRecord() { if (isFirstRecord) { if (includeHeader) { - final List uniqueHeader = header.stream().distinct().collect(Collectors.toList()); - writeRow(uniqueHeader); + writeRow(header); header.clear(); } isFirstRecord = false; From 96605ab3432eb2823b4652a1350cc892be5bc07e Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Fri, 19 May 2023 19:02:28 +0200 Subject: [PATCH 5/7] Use stream test; format (#483) --- .../java/org/metafacture/csv/CsvEncoder.java | 11 +- .../org/metafacture/csv/CsvEncoderTest.java | 207 +++++++++--------- 2 files changed, 106 insertions(+), 112 deletions(-) diff --git a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java index 513884dd0..3723493db 100644 --- a/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java +++ b/metafacture-csv/src/main/java/org/metafacture/csv/CsvEncoder.java @@ -31,7 +31,6 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** * A csv encoder that converts a record into a csv line (Default separator: {@value #DEFAULT_SEP}). @@ -139,11 +138,9 @@ public void setNoQuotes(final boolean noQuotes) { private void initialize() { writer = new StringWriter(); final String emptyLineEnd = ""; - csvWriter = new CSVWriter(writer, - separator, - noQuotes ? CSVWriter.NO_QUOTE_CHARACTER : CSVWriter.DEFAULT_QUOTE_CHARACTER, - CSVWriter.DEFAULT_ESCAPE_CHARACTER, - emptyLineEnd); + csvWriter = new CSVWriter(writer, separator, + noQuotes ? CSVWriter.NO_QUOTE_CHARACTER : CSVWriter.DEFAULT_QUOTE_CHARACTER, + CSVWriter.DEFAULT_ESCAPE_CHARACTER, emptyLineEnd); } private String[] arrayOf(final List list) { @@ -190,7 +187,6 @@ public void endRecord() { } writeRow(rowItems); - resetCaches(); } @@ -217,7 +213,6 @@ public void onResetStream() { this.includeRecordId = false; this.includeHeader = false; this.header = new ArrayList<>(); - this.isFirstRecord = true; this.rowItems = new ArrayList<>(); } diff --git a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java index e36e07006..78b438e53 100644 --- a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java +++ b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java @@ -16,165 +16,164 @@ package org.metafacture.csv; -import org.junit.After; +import org.metafacture.framework.ObjectReceiver; + import org.junit.Before; import org.junit.Test; -import org.metafacture.framework.ObjectReceiver; + import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.exceptions.base.MockitoAssertionError; -import static org.mockito.Mockito.inOrder; +import java.util.Arrays; +import java.util.function.Consumer; /** * Tests for {@link CsvEncoder}. * * @author eberhardtj (j.eberhardt@dnb.de) * @author Pascal Christoph (dr0i) + * @author Jens Wille */ public final class CsvEncoderTest { - private CsvEncoder encoder; - @Mock private ObjectReceiver receiver; + private static final String LITERAL1 = "column 1"; + private static final String LITERAL2 = "column 2"; + private static final String RECORD_ID1 = "1"; + private static final String RECORD_ID2 = "2"; + private static final String RECORD_ID3 = "3"; + private static final String VALUE1 = "a"; + private static final String VALUE2 = "b"; + private static final String VALUE3 = "c"; + private static final String VALUE4 = "d"; + private static final String VALUE5 = "e"; + private static final String VALUE6 = "f"; @Before public void setup() { MockitoAnnotations.initMocks(this); - encoder = new CsvEncoder(); - encoder.setIncludeHeader(false); + CsvEncoder encoder = new CsvEncoder(); encoder.setReceiver(receiver); } - @After - public void cleanup() { - encoder.closeStream(); - } @Test public void shouldReceiveSingleRecord() { - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"a\",\"b\""); + assertEncode(i -> { + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "\"a\",\"b\""); } @Test public void shouldHaveNoQuotes() { - encoder.setNoQuotes(true); - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("a,b"); + assertEncode(i -> { + i.setNoQuotes(true); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "a,b"); } @Test public void shouldReceiveSingleRecordWithHeader() { - encoder.setIncludeHeader(true); - - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"column 1\",\"column 2\""); - ordered.verify(receiver).process("\"a\",\"b\""); + assertEncode(i -> { + i.setIncludeHeader(true); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "\"column 1\",\"column 2\"", "\"a\",\"b\""); } @Test public void shouldReceiveSingleRecordWithRecordId() { - encoder.setIncludeRecordId(true); - - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"1\",\"a\",\"b\""); + assertEncode(i -> { + i.setIncludeRecordId(true); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "\"1\",\"a\",\"b\""); } @Test public void shouldReceiveSingleRecordWithRecordIdAndHeader() { - encoder.setIncludeRecordId(true); - encoder.setIncludeHeader(true); - - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"record id\",\"column 1\",\"column 2\""); - ordered.verify(receiver).process("\"1\",\"a\",\"b\""); + assertEncode(i -> { + i.setIncludeRecordId(true); + i.setIncludeHeader(true); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "\"record id\",\"column 1\",\"column 2\"", "\"1\",\"a\",\"b\""); } @Test public void shouldReceiveThreeRows() { - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.startRecord("2"); - encoder.literal("column 1", "c"); - encoder.literal("column 2", "d"); - encoder.endRecord(); - encoder.startRecord("3"); - encoder.literal("column 1", "e"); - encoder.literal("column 2", "f"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"a\",\"b\""); - ordered.verify(receiver).process("\"c\",\"d\""); - ordered.verify(receiver).process("\"e\",\"f\""); + assertEncode(i -> { + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + i.startRecord(RECORD_ID2); + i.literal(LITERAL1, VALUE3); + i.literal(LITERAL2, VALUE4); + i.endRecord(); + i.startRecord(RECORD_ID3); + i.literal(LITERAL1, VALUE5); + i.literal(LITERAL2, VALUE6); + i.endRecord(); + }, "\"a\",\"b\"", "\"c\",\"d\"", "\"e\",\"f\""); } @Test public void shouldUseTabulatorAsSeparator() { - encoder.setSeparator('\t'); - - encoder.startRecord("1"); - encoder.literal("column 1", "a"); - encoder.literal("column 2", "b"); - encoder.endRecord(); - encoder.startRecord("2"); - encoder.literal("column 1", "c"); - encoder.literal("column 2", "d"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"a\"\t\"b\""); - ordered.verify(receiver).process("\"c\"\t\"d\""); + assertEncode(i -> { + i.setSeparator('\t'); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.endRecord(); + }, "\"a\"\t\"b\""); } @Test public void shouldNotCreateNestedCsvInColumn() { - encoder.startRecord("1"); - encoder.literal("name", "a"); - encoder.literal("alias", "a1"); - encoder.literal("alias", "a2"); - encoder.literal("alias", "a3"); - encoder.endRecord(); - encoder.closeStream(); - - final InOrder ordered = inOrder(receiver); - ordered.verify(receiver).process("\"a\",\"a1\",\"a2\",\"a3\""); + assertEncode(i -> { + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.literal(LITERAL2, VALUE3); + i.literal(LITERAL2, VALUE4); + i.endRecord(); + }, "\"a\",\"b\",\"c\",\"d\""); } + private void assertEncode(final Consumer in, final String... out) { + final InOrder ordered = Mockito.inOrder(receiver); + + final CsvEncoder csvEncoder = new CsvEncoder(); + csvEncoder.setReceiver(receiver); + in.accept(csvEncoder); + + try { + Arrays.stream(out).forEach(s -> ordered.verify(receiver).process(s)); + + ordered.verifyNoMoreInteractions(); + Mockito.verifyNoMoreInteractions(receiver); + } + catch (final MockitoAssertionError e) { + System.out.println(Mockito.mockingDetails(receiver).printInvocations()); + throw e; + } + + } } From f3b3d92e162b45cc923daf169d41a0db371d8384 Mon Sep 17 00:00:00 2001 From: Jens Wille Date: Mon, 22 May 2023 10:36:15 +0200 Subject: [PATCH 6/7] Add unit test for `CsvEncoder`. (#486) (96811da) --- .../java/org/metafacture/csv/CsvEncoderTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java index 78b438e53..3dba383a7 100644 --- a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java +++ b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java @@ -157,6 +157,20 @@ public void shouldNotCreateNestedCsvInColumn() { }, "\"a\",\"b\",\"c\",\"d\""); } + @Test + public void shouldRepeatHeaderForRepeatedColumns() { + assertEncode(i -> { + i.setIncludeHeader(true); + i.startRecord(RECORD_ID1); + i.literal(LITERAL1, VALUE1); + i.literal(LITERAL2, VALUE2); + i.literal(LITERAL2, VALUE3); + i.literal(LITERAL1, VALUE4); + i.literal(LITERAL2, VALUE5); + i.endRecord(); + }, "\"column 1\",\"column 2\",\"column 2\",\"column 1\",\"column 2\"", "\"a\",\"b\",\"c\",\"d\",\"e\""); + } + private void assertEncode(final Consumer in, final String... out) { final InOrder ordered = Mockito.inOrder(receiver); From 67b44124e1a7a3ed16b25ab2aac85a30a0234b49 Mon Sep 17 00:00:00 2001 From: Pascal Christoph Date: Mon, 22 May 2023 11:56:13 +0200 Subject: [PATCH 7/7] Use MockitoRule instead of @before setup (#483) Based on a suggestion by Jens Wille. --- .../java/org/metafacture/csv/CsvEncoderTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java index 3dba383a7..2356d1f43 100644 --- a/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java +++ b/metafacture-csv/src/test/java/org/metafacture/csv/CsvEncoderTest.java @@ -16,6 +16,7 @@ package org.metafacture.csv; +import org.junit.Rule; import org.metafacture.framework.ObjectReceiver; import org.junit.Before; @@ -26,6 +27,8 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.exceptions.base.MockitoAssertionError; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.function.Consumer; @@ -53,13 +56,8 @@ public final class CsvEncoderTest { private static final String VALUE5 = "e"; private static final String VALUE6 = "f"; - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - CsvEncoder encoder = new CsvEncoder(); - encoder.setReceiver(receiver); - } - + @Rule + public MockitoRule rule = MockitoJUnit.rule(); @Test public void shouldReceiveSingleRecord() {