From 0f6bf93af595bf2d89223b9869fd06eadc8d1c66 Mon Sep 17 00:00:00 2001 From: Oliver Siegmar Date: Thu, 19 Sep 2024 21:18:07 +0200 Subject: [PATCH] add multiple flushing capabilities --- CHANGELOG.md | 5 +- .../example/ExampleCsvWriterWithComments.java | 7 +- ...xampleCsvWriterWithDataTransformation.java | 7 +- ...riterWithNonStandardControlCharacters.java | 8 +- .../ExampleCsvWriterWithQuoteStrategy.java | 23 +-- .../ExampleCsvWriterWithSingleFields.java | 7 +- .../ExampleCsvWriterWithStringOutput.java | 3 +- .../blackbox/writer/ConsoleWriterTest.java | 40 +++++ .../java/blackbox/writer/CsvWriterTest.java | 41 ++++- .../de/siegmar/fastcsv/writer/CsvWriter.java | 157 ++++++++---------- .../fastcsv/writer/FastBufferedWriter.java | 87 ++++++++++ .../siegmar/fastcsv/writer/NoCloseWriter.java | 20 +++ .../fastcsv/writer/UnbufferedWriter.java | 27 +++ .../de/siegmar/fastcsv/writer/Writable.java | 44 +++++ .../writer/FastBufferedWriterTest.java | 51 +++++- .../fastcsv/writer/NoCloseWriterTest.java | 23 +++ 16 files changed, 416 insertions(+), 134 deletions(-) create mode 100644 lib/src/intTest/java/blackbox/writer/ConsoleWriterTest.java create mode 100644 lib/src/main/java/de/siegmar/fastcsv/writer/FastBufferedWriter.java create mode 100644 lib/src/main/java/de/siegmar/fastcsv/writer/NoCloseWriter.java create mode 100644 lib/src/main/java/de/siegmar/fastcsv/writer/UnbufferedWriter.java create mode 100644 lib/src/main/java/de/siegmar/fastcsv/writer/Writable.java create mode 100644 lib/src/test/java/de/siegmar/fastcsv/writer/NoCloseWriterTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 172768b0..d2e7325a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -- Nothing yet +### Added +- Implement `Flushable` interface for `CsvWriter` to allow flushing the underlying writer +- Implement `autoFlush` option for `CsvWriter` to automatically flush the writer after writing a record +- Implement `toConsole` method for `CsvWriter` to write records to the console ## [3.2.0] - 2024-06-15 ### Added diff --git a/example/src/main/java/example/ExampleCsvWriterWithComments.java b/example/src/main/java/example/ExampleCsvWriterWithComments.java index ea1c59af..d2b6c6fa 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithComments.java +++ b/example/src/main/java/example/ExampleCsvWriterWithComments.java @@ -1,7 +1,5 @@ package example; -import java.io.StringWriter; - import de.siegmar.fastcsv.writer.CsvWriter; /** @@ -10,13 +8,10 @@ public class ExampleCsvWriterWithComments { public static void main(final String[] args) { - final StringWriter sw = new StringWriter(); - CsvWriter.builder().build(sw) + CsvWriter.builder().toConsole() .writeComment("A comment can be placed\nanywhere") .writeRecord("field 1", "field 2", "field 3\n#with a line break") .writeComment("in the CSV file"); - - System.out.println(sw); } } diff --git a/example/src/main/java/example/ExampleCsvWriterWithDataTransformation.java b/example/src/main/java/example/ExampleCsvWriterWithDataTransformation.java index 524e14dc..baa209d1 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithDataTransformation.java +++ b/example/src/main/java/example/ExampleCsvWriterWithDataTransformation.java @@ -1,7 +1,6 @@ package example; import java.io.IOException; -import java.io.StringWriter; import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.NamedCsvRecord; @@ -15,10 +14,8 @@ public class ExampleCsvWriterWithDataTransformation { private static final String DATA = "firstname,initial,lastname,age\njohn,h.,smith"; public static void main(final String[] args) throws IOException { - final StringWriter out = new StringWriter(); - try (CsvReader reader = CsvReader.builder().ofNamedCsvRecord(DATA); - CsvWriter writer = CsvWriter.builder().build(out)) { + CsvWriter writer = CsvWriter.builder().toConsole()) { // transform firstname, initial, lastname to lastname, firstname writer.writeRecord("lastname", "firstname"); @@ -28,8 +25,6 @@ public static void main(final String[] args) throws IOException { writer.writeRecord(lastname, firstname); } } - - System.out.println(out); } } diff --git a/example/src/main/java/example/ExampleCsvWriterWithNonStandardControlCharacters.java b/example/src/main/java/example/ExampleCsvWriterWithNonStandardControlCharacters.java index 39f97605..c9f9b3fa 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithNonStandardControlCharacters.java +++ b/example/src/main/java/example/ExampleCsvWriterWithNonStandardControlCharacters.java @@ -1,7 +1,5 @@ package example; -import java.io.StringWriter; - import de.siegmar.fastcsv.writer.CsvWriter; import de.siegmar.fastcsv.writer.LineDelimiter; @@ -11,8 +9,6 @@ public class ExampleCsvWriterWithNonStandardControlCharacters { public static void main(final String[] args) { - final StringWriter sw = new StringWriter(); - // The default configuration uses a comma as field separator, // a double quote as quote character and // a CRLF as line delimiter. @@ -20,11 +16,9 @@ public static void main(final String[] args) { .fieldSeparator(';') .quoteCharacter('\'') .lineDelimiter(LineDelimiter.LF) - .build(sw) + .toConsole() .writeRecord("header1", "header2") .writeRecord("value1", "value;2"); - - System.out.println(sw); } } diff --git a/example/src/main/java/example/ExampleCsvWriterWithQuoteStrategy.java b/example/src/main/java/example/ExampleCsvWriterWithQuoteStrategy.java index f78216e3..06d9440d 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithQuoteStrategy.java +++ b/example/src/main/java/example/ExampleCsvWriterWithQuoteStrategy.java @@ -1,8 +1,5 @@ package example; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; - import de.siegmar.fastcsv.writer.CsvWriter; import de.siegmar.fastcsv.writer.QuoteStrategies; import de.siegmar.fastcsv.writer.QuoteStrategy; @@ -13,33 +10,29 @@ public class ExampleCsvWriterWithQuoteStrategy { public static void main(final String[] args) { - final PrintWriter pw = new PrintWriter(System.out, false, StandardCharsets.UTF_8); - - pw.println("Quote always"); + System.out.println("Quote always"); CsvWriter.builder() .quoteStrategy(QuoteStrategies.ALWAYS) - .build(pw) + .toConsole() .writeRecord("value1", "", null, "value,4"); - pw.println("Quote non-empty"); + System.out.println("Quote non-empty"); CsvWriter.builder() .quoteStrategy(QuoteStrategies.NON_EMPTY) - .build(pw) + .toConsole() .writeRecord("value1", "", null, "value,4"); - pw.println("Quote empty"); + System.out.println("Quote empty"); CsvWriter.builder() .quoteStrategy(QuoteStrategies.EMPTY) - .build(pw) + .toConsole() .writeRecord("value1", "", null, "value,4"); - pw.println("Quote custom"); + System.out.println("Quote custom"); CsvWriter.builder() .quoteStrategy(customQuote()) - .build(pw) + .toConsole() .writeRecord("value1", "", null, "value,4"); - - pw.flush(); } // A quote strategy can be used to force quote fields that would otherwise not be quoted. diff --git a/example/src/main/java/example/ExampleCsvWriterWithSingleFields.java b/example/src/main/java/example/ExampleCsvWriterWithSingleFields.java index c79efd00..7a22bb1b 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithSingleFields.java +++ b/example/src/main/java/example/ExampleCsvWriterWithSingleFields.java @@ -1,7 +1,5 @@ package example; -import java.io.StringWriter; - import de.siegmar.fastcsv.writer.CsvWriter; /** @@ -10,12 +8,9 @@ public class ExampleCsvWriterWithSingleFields { public static void main(final String[] args) { - final StringWriter sw = new StringWriter(); - CsvWriter.builder().build(sw) + CsvWriter.builder().toConsole() .writeRecord("header1", "header2") .writeRecord().writeField("value1").writeField("value2").endRecord(); - - System.out.println(sw); } } diff --git a/example/src/main/java/example/ExampleCsvWriterWithStringOutput.java b/example/src/main/java/example/ExampleCsvWriterWithStringOutput.java index 2afc9d7a..dc2cc4e4 100644 --- a/example/src/main/java/example/ExampleCsvWriterWithStringOutput.java +++ b/example/src/main/java/example/ExampleCsvWriterWithStringOutput.java @@ -15,7 +15,8 @@ public static void main(final String[] args) { .writeRecord("header1", "header2") .writeRecord("value1", "value2"); - System.out.println(sw); + final String csv = sw.toString(); + System.out.println(csv); } } diff --git a/lib/src/intTest/java/blackbox/writer/ConsoleWriterTest.java b/lib/src/intTest/java/blackbox/writer/ConsoleWriterTest.java new file mode 100644 index 00000000..bc7244a9 --- /dev/null +++ b/lib/src/intTest/java/blackbox/writer/ConsoleWriterTest.java @@ -0,0 +1,40 @@ +package blackbox.writer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.siegmar.fastcsv.writer.CsvWriter; + +class ConsoleWriterTest { + + @SuppressWarnings("checkstyle:RegexpMultiline") + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream capturedOut = new ByteArrayOutputStream(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(capturedOut, true, StandardCharsets.UTF_8)); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } + + @Test + void console() { + CsvWriter.builder().toConsole() + .writeRecord("foo", "bar"); + + assertThat(capturedOut).asString() + .isEqualTo("foo,bar\r\n"); + } + +} diff --git a/lib/src/intTest/java/blackbox/writer/CsvWriterTest.java b/lib/src/intTest/java/blackbox/writer/CsvWriterTest.java index 74498db3..8477ae9a 100644 --- a/lib/src/intTest/java/blackbox/writer/CsvWriterTest.java +++ b/lib/src/intTest/java/blackbox/writer/CsvWriterTest.java @@ -1,11 +1,14 @@ package blackbox.writer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.io.FilterWriter; import java.io.IOException; import java.io.StringWriter; import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -260,13 +263,49 @@ void disableBuffer() { assertThat(stringWriter).asString().isEqualTo("foo,bar\n"); } + // autoFlush + + @Test + void noAutoFlush() { + final CsvWriter csvWriter = CsvWriter.builder().build(flushFailWriter()); + assertThatCode(() -> csvWriter.writeRecord("foo")) + .doesNotThrowAnyException(); + } + + @Test + void manualFlush(@TempDir final Path tempDir) throws IOException { + final Path file = tempDir.resolve("fastcsv.csv"); + CsvWriter.builder().build(file) + .writeRecord("foo") + .flush(); + + assertThat(Files.readString(file)) + .isEqualTo("foo\r\n"); + } + + @Test + void autoFlush() { + final CsvWriter csvWriter = CsvWriter.builder().autoFlush(true).build(flushFailWriter()); + assertThatThrownBy(() -> csvWriter.writeRecord("foo")) + .isInstanceOf(UnsupportedOperationException.class); + } + + private static FilterWriter flushFailWriter() { + return new FilterWriter(FilterWriter.nullWriter()) { + @Override + public void flush() { + throw new UnsupportedOperationException(); + } + }; + } + // toString() @Test void builderToString() { assertThat(crw).asString() .isEqualTo("CsvWriterBuilder[fieldSeparator=,, quoteCharacter=\", " - + "commentCharacter=#, quoteStrategy=null, lineDelimiter=\n, bufferSize=8192]"); + + "commentCharacter=#, quoteStrategy=null, lineDelimiter=\n, bufferSize=8192, autoFlush=false]"); } @Test diff --git a/lib/src/main/java/de/siegmar/fastcsv/writer/CsvWriter.java b/lib/src/main/java/de/siegmar/fastcsv/writer/CsvWriter.java index e9b7c067..dbf8933b 100644 --- a/lib/src/main/java/de/siegmar/fastcsv/writer/CsvWriter.java +++ b/lib/src/main/java/de/siegmar/fastcsv/writer/CsvWriter.java @@ -5,6 +5,7 @@ import static de.siegmar.fastcsv.util.Util.containsDupe; import java.io.Closeable; +import java.io.Flushable; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UncheckedIOException; @@ -31,23 +32,22 @@ *} */ @SuppressWarnings({"checkstyle:NPathComplexity", "checkstyle:CyclomaticComplexity"}) -public final class CsvWriter implements Closeable { +public final class CsvWriter implements Closeable, Flushable { - private final Writer writer; + private final Writable writer; private final char fieldSeparator; private final char quoteCharacter; private final char commentCharacter; private final QuoteStrategy quoteStrategy; private final LineDelimiter lineDelimiter; - private final boolean flushWriter; private int currentLineNo = 1; private final char[] lineDelimiterChars; private final char[] emptyFieldValue; private boolean openRecordWriter; - CsvWriter(final Writer writer, final char fieldSeparator, final char quoteCharacter, - final char commentCharacter, final QuoteStrategy quoteStrategy, final LineDelimiter lineDelimiter, - final boolean flushWriter) { + @SuppressWarnings("checkstyle:ParameterNumber") + CsvWriter(final Writable writer, final char fieldSeparator, final char quoteCharacter, + final char commentCharacter, final QuoteStrategy quoteStrategy, final LineDelimiter lineDelimiter) { Preconditions.checkArgument(!Util.isNewline(fieldSeparator), "fieldSeparator must not be a newline char"); Preconditions.checkArgument(!Util.isNewline(quoteCharacter), "quoteCharacter must not be a newline char"); Preconditions.checkArgument(!Util.isNewline(commentCharacter), "commentCharacter must not be a newline char"); @@ -61,7 +61,6 @@ public final class CsvWriter implements Closeable { this.commentCharacter = commentCharacter; this.quoteStrategy = quoteStrategy; this.lineDelimiter = Objects.requireNonNull(lineDelimiter); - this.flushWriter = flushWriter; emptyFieldValue = new char[] {quoteCharacter, quoteCharacter}; lineDelimiterChars = lineDelimiter.toString().toCharArray(); @@ -153,7 +152,7 @@ private void writeInternal(final String value, final int fieldIdx) throws IOExce if (value == null) { if (quoteStrategy != null && quoteStrategy.quoteNull(currentLineNo, fieldIdx)) { - writer.write(emptyFieldValue); + writer.write(emptyFieldValue, 0, emptyFieldValue.length); } return; } @@ -162,7 +161,7 @@ private void writeInternal(final String value, final int fieldIdx) throws IOExce if (length == 0) { if (quoteStrategy != null && quoteStrategy.quoteEmpty(currentLineNo, fieldIdx)) { - writer.write(emptyFieldValue); + writer.write(emptyFieldValue, 0, emptyFieldValue.length); } return; } @@ -215,7 +214,7 @@ private boolean containsControlCharacter(final String value, final int fieldIdx, return false; } - private static void writeEscaped(final Writer w, final String value, final char quoteChar) + private static void writeEscaped(final Writable w, final String value, final char quoteChar) throws IOException { int startPos = 0; @@ -297,9 +296,8 @@ private void writeFragment(final String comment, final int i, final int startPos private CsvWriter endRecord() throws IOException { ++currentLineNo; writer.write(lineDelimiterChars, 0, lineDelimiterChars.length); - if (flushWriter) { - writer.flush(); - } + writer.endRecord(); + return this; } @@ -308,6 +306,11 @@ public void close() throws IOException { writer.close(); } + @Override + public void flush() throws IOException { + writer.flush(); + } + @Override public String toString() { return new StringJoiner(", ", CsvWriter.class.getSimpleName() + "[", "]") @@ -329,6 +332,7 @@ public String toString() { *
  • quote strategy: {@code null} (only required quoting)
  • *
  • line delimiter: {@link LineDelimiter#CRLF}
  • *
  • buffer size: 8,192 bytes
  • + *
  • auto flush: {@code false}
  • * */ @SuppressWarnings({"checkstyle:HiddenField", "PMD.AvoidFieldNameMatchingMethodName"}) @@ -342,6 +346,7 @@ public static final class CsvWriterBuilder { private QuoteStrategy quoteStrategy; private LineDelimiter lineDelimiter = LineDelimiter.CRLF; private int bufferSize = DEFAULT_BUFFER_SIZE; + private boolean autoFlush; CsvWriterBuilder() { } @@ -412,6 +417,8 @@ public CsvWriterBuilder lineDelimiter(final LineDelimiter lineDelimiter) { * need many instances of a CsvWriter and need to optimize for instantiation time and memory footprint. *

    * A buffer size of 0 disables the buffer. + *

    + * This setting is ignored when using {@link #toConsole()} as console output is unbuffered. * * @param bufferSize the buffer size to be used (must be ≥ 0). * @return This updated object, allowing additional method calls to be chained together. @@ -422,6 +429,21 @@ public CsvWriterBuilder bufferSize(final int bufferSize) { return this; } + /** + * Configures whether data should be flushed after each record write operation. + *

    + * Obviously this comes with drastic performance implications but can be useful for debugging purposes. + *

    + * This setting is ignored when using {@link #toConsole()} as console output is always flushed. + * + * @param autoFlush whether the data should be flushed after each record write operation. + * @return This updated object, allowing additional method calls to be chained together. + */ + public CsvWriterBuilder autoFlush(final boolean autoFlush) { + this.autoFlush = autoFlush; + return this; + } + /** * Constructs a {@link CsvWriter} for the specified Writer. *

    @@ -437,7 +459,7 @@ public CsvWriterBuilder bufferSize(final int bufferSize) { public CsvWriter build(final Writer writer) { Objects.requireNonNull(writer, "writer must not be null"); - return newWriter(writer, true); + return csvWriter(writer, bufferSize, true, autoFlush); } /** @@ -473,18 +495,40 @@ public CsvWriter build(final Path file, final Charset charset, Objects.requireNonNull(file, "file must not be null"); Objects.requireNonNull(charset, "charset must not be null"); - return newWriter(new OutputStreamWriter(Files.newOutputStream(file, openOptions), - charset), false); + return csvWriter(new OutputStreamWriter(Files.newOutputStream(file, openOptions), + charset), bufferSize, false, autoFlush); } - private CsvWriter newWriter(final Writer writer, final boolean flushWriter) { - if (bufferSize > 0) { - return new CsvWriter(new FastBufferedWriter(writer, bufferSize), fieldSeparator, quoteCharacter, - commentCharacter, quoteStrategy, lineDelimiter, flushWriter); - } + /** + * Convenience method to write to the console (standard output). + *

    + * Settings {@link #bufferSize(int)} and {@link #autoFlush(boolean)} are ignored. + * Data is directly written to standard output and flushed after each record. + *

    + * Example use: + *

    + * {@snippet : + * CsvWriter.builder().toConsole() + * .writeRecord("Hello", "world"); + *} + * + * @return a new CsvWriter instance - never {@code null}. + * Calls to {@link #close()} are ignored, standard out remains open. + */ + @SuppressWarnings("checkstyle:RegexpMultiline") + public CsvWriter toConsole() { + final Writer writer = new NoCloseWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + return csvWriter(writer, 0, false, true); + } + + private CsvWriter csvWriter(final Writer writer, final int bufferSize, + final boolean autoFlushBuffer, final boolean autoFlushWriter) { + final Writable writable = bufferSize > 0 + ? new FastBufferedWriter(writer, bufferSize, autoFlushBuffer, autoFlushWriter) + : new UnbufferedWriter(writer, autoFlushWriter); - return new CsvWriter(writer, fieldSeparator, quoteCharacter, - commentCharacter, quoteStrategy, lineDelimiter, false); + return new CsvWriter(writable, + fieldSeparator, quoteCharacter, commentCharacter, quoteStrategy, lineDelimiter); } @Override @@ -496,77 +540,12 @@ public String toString() { .add("quoteStrategy=" + quoteStrategy) .add("lineDelimiter=" + lineDelimiter) .add("bufferSize=" + bufferSize) + .add("autoFlush=" + autoFlush) .toString(); } } - /** - * High-performance buffered writer (without synchronization). - *

    - * This class is intended for internal use only. - */ - static final class FastBufferedWriter extends Writer { - - private final Writer writer; - private final char[] buf; - private int pos; - - FastBufferedWriter(final Writer writer, final int bufferSize) { - this.writer = writer; - buf = new char[bufferSize]; - } - - @Override - public void write(final int c) throws IOException { - if (pos == buf.length) { - flush(); - } - buf[pos++] = (char) c; - } - - @Override - public void write(final char[] cbuf, final int off, final int len) throws IOException { - if (pos + len >= buf.length) { - flush(); - if (len >= buf.length) { - writer.write(cbuf, off, len); - return; - } - } - - System.arraycopy(cbuf, off, buf, pos, len); - pos += len; - } - - @Override - public void write(final String str, final int off, final int len) throws IOException { - if (pos + len >= buf.length) { - flush(); - if (len >= buf.length) { - writer.write(str, off, len); - return; - } - } - - str.getChars(off, off + len, buf, pos); - pos += len; - } - - @Override - public void flush() throws IOException { - writer.write(buf, 0, pos); - pos = 0; - } - - @Override - public void close() throws IOException { - flush(); - writer.close(); - } - - } - /** * This class is used to write a record field by field. *

    diff --git a/lib/src/main/java/de/siegmar/fastcsv/writer/FastBufferedWriter.java b/lib/src/main/java/de/siegmar/fastcsv/writer/FastBufferedWriter.java new file mode 100644 index 00000000..e45a2798 --- /dev/null +++ b/lib/src/main/java/de/siegmar/fastcsv/writer/FastBufferedWriter.java @@ -0,0 +1,87 @@ +package de.siegmar.fastcsv.writer; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * High-performance buffered writer (without synchronization). + */ +final class FastBufferedWriter extends FilterWriter implements Writable { + + private final char[] buf; + private final boolean autoFlushBuffer; + private final boolean autoFlushWriter; + private int pos; + + FastBufferedWriter(final Writer writer, final int bufferSize, + final boolean autoFlushBuffer, final boolean autoFlushWriter) { + super(writer); + buf = new char[bufferSize]; + this.autoFlushBuffer = autoFlushBuffer; + this.autoFlushWriter = autoFlushWriter; + } + + @Override + public void write(final int c) throws IOException { + if (pos == buf.length) { + flushBuffer(); + } + buf[pos++] = (char) c; + } + + @Override + public void write(final char[] cbuf, final int off, final int len) throws IOException { + if (pos + len >= buf.length) { + flushBuffer(); + if (len >= buf.length) { + out.write(cbuf, off, len); + return; + } + } + + System.arraycopy(cbuf, off, buf, pos, len); + pos += len; + } + + @Override + public void write(final String str, final int off, final int len) throws IOException { + if (pos + len >= buf.length) { + flushBuffer(); + if (len >= buf.length) { + out.write(str, off, len); + return; + } + } + + str.getChars(off, off + len, buf, pos); + pos += len; + } + + @Override + public void endRecord() throws IOException { + if (autoFlushWriter) { + flush(); + } else if (autoFlushBuffer) { + flushBuffer(); + } + } + + private void flushBuffer() throws IOException { + out.write(buf, 0, pos); + pos = 0; + } + + @Override + public void flush() throws IOException { + flushBuffer(); + super.flush(); + } + + @Override + public void close() throws IOException { + flushBuffer(); + super.close(); + } + +} diff --git a/lib/src/main/java/de/siegmar/fastcsv/writer/NoCloseWriter.java b/lib/src/main/java/de/siegmar/fastcsv/writer/NoCloseWriter.java new file mode 100644 index 00000000..27c01a24 --- /dev/null +++ b/lib/src/main/java/de/siegmar/fastcsv/writer/NoCloseWriter.java @@ -0,0 +1,20 @@ +package de.siegmar.fastcsv.writer; + +import java.io.FilterWriter; +import java.io.Writer; + +/** + * A writer that does not close the underlying writer. + */ +class NoCloseWriter extends FilterWriter { + + NoCloseWriter(final Writer out) { + super(out); + } + + @Override + public void close() { + // do nothing + } + +} diff --git a/lib/src/main/java/de/siegmar/fastcsv/writer/UnbufferedWriter.java b/lib/src/main/java/de/siegmar/fastcsv/writer/UnbufferedWriter.java new file mode 100644 index 00000000..62fa2959 --- /dev/null +++ b/lib/src/main/java/de/siegmar/fastcsv/writer/UnbufferedWriter.java @@ -0,0 +1,27 @@ +package de.siegmar.fastcsv.writer; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Implementation of {@link Writable} that does not buffer any data + * but flushes the underlying writer at the end of each record if configured. + */ +final class UnbufferedWriter extends FilterWriter implements Writable { + + private final boolean autoFlushWriter; + + UnbufferedWriter(final Writer out, final boolean autoFlushWriter) { + super(out); + this.autoFlushWriter = autoFlushWriter; + } + + @Override + public void endRecord() throws IOException { + if (autoFlushWriter) { + flush(); + } + } + +} diff --git a/lib/src/main/java/de/siegmar/fastcsv/writer/Writable.java b/lib/src/main/java/de/siegmar/fastcsv/writer/Writable.java new file mode 100644 index 00000000..4c2cc420 --- /dev/null +++ b/lib/src/main/java/de/siegmar/fastcsv/writer/Writable.java @@ -0,0 +1,44 @@ +package de.siegmar.fastcsv.writer; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; + +/** + * This interface extends the basic functionality provided by {@link java.io.Writer} + * with the addition of the {@link #endRecord()} method. + */ +interface Writable extends Closeable, Flushable { + + /** + * Writes a single character. + * @param c the character to write + * @see java.io.Writer#write(int) + */ + void write(int c) throws IOException; + + /** + * Writes a portion of a string. + * @param value the string to write + * @param off the offset from which to start writing characters + * @param len the number of characters to write + * @see java.io.Writer#write(String, int, int) + */ + void write(String value, int off, int len) throws IOException; + + /** + * Writes a portion of an array of characters. + * @param value the array of characters to write + * @param off the offset from which to start writing characters + * @param len the number of characters to write + * @see java.io.Writer#write(char[], int, int) + */ + void write(char[] value, int off, int len) throws IOException; + + /** + * Called to indicate that the current record is complete. + * @throws IOException if an I/O error occurs + */ + void endRecord() throws IOException; + +} diff --git a/lib/src/test/java/de/siegmar/fastcsv/writer/FastBufferedWriterTest.java b/lib/src/test/java/de/siegmar/fastcsv/writer/FastBufferedWriterTest.java index b2603bb2..dfe0f825 100644 --- a/lib/src/test/java/de/siegmar/fastcsv/writer/FastBufferedWriterTest.java +++ b/lib/src/test/java/de/siegmar/fastcsv/writer/FastBufferedWriterTest.java @@ -4,13 +4,14 @@ import java.io.IOException; import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; class FastBufferedWriterTest { private final StringWriter sw = new StringWriter(); - private final CsvWriter.FastBufferedWriter cw = new CsvWriter.FastBufferedWriter(sw, 8192); + private final FastBufferedWriter cw = new FastBufferedWriter(sw, 8192, false, false); @Test void appendSingle() throws IOException { @@ -56,13 +57,59 @@ void appendString() throws IOException { } @Test - void appendLarge() throws IOException { + void appendLargeString() throws IOException { final String sb = buildLargeData(); cw.write(sb, 0, sb.length()); assertThat(sw).asString().isEqualTo(sb); } + @Test + void appendLargeArray() throws IOException { + final char[] data = buildLargeData().toCharArray(); + cw.write(data, 0, data.length); + + assertThat(sw).asString().isEqualTo(new String(data)); + } + + @Test + void autoFlushBuffer() throws IOException { + final var stringWriter = new StringWriter() { + @Override + public void flush() { + throw new UnsupportedOperationException(); + } + }; + + final var fbw = new FastBufferedWriter(stringWriter, 8, true, false); + + fbw.write("foo"); + assertThat(stringWriter).asString().isEmpty(); + + fbw.endRecord(); + assertThat(stringWriter).asString().isEqualTo("foo"); + } + + @Test + void autoFlushWriter() throws IOException { + final AtomicInteger flushCount = new AtomicInteger(); + final var stringWriter = new StringWriter() { + @Override + public void flush() { + flushCount.incrementAndGet(); + } + }; + + final var fbw = new FastBufferedWriter(stringWriter, 8, true, true); + + fbw.write("foo"); + assertThat(stringWriter).asString().isEmpty(); + + fbw.endRecord(); + assertThat(stringWriter).asString().isEqualTo("foo"); + assertThat(flushCount).hasValue(1); + } + private String buildLargeData() { return "ab".repeat(8192); } diff --git a/lib/src/test/java/de/siegmar/fastcsv/writer/NoCloseWriterTest.java b/lib/src/test/java/de/siegmar/fastcsv/writer/NoCloseWriterTest.java new file mode 100644 index 00000000..30e2b11c --- /dev/null +++ b/lib/src/test/java/de/siegmar/fastcsv/writer/NoCloseWriterTest.java @@ -0,0 +1,23 @@ +package de.siegmar.fastcsv.writer; + +import static org.assertj.core.api.Assertions.fail; + +import java.io.FilterWriter; + +import org.junit.jupiter.api.Test; + +class NoCloseWriterTest { + + @Test + void noClose() { + final var closeFailWriter = new FilterWriter(FilterWriter.nullWriter()) { + @Override + public void close() { + fail(); + } + }; + + new NoCloseWriter(closeFailWriter).close(); + } + +}