
de.siegmar.fastcsv.writer.CsvWriter Maven / Gradle / Ivy
Show all versions of fastcsv Show documentation
package de.siegmar.fastcsv.writer;
import static de.siegmar.fastcsv.util.Util.CR;
import static de.siegmar.fastcsv.util.Util.LF;
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;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.Objects;
import java.util.StringJoiner;
import de.siegmar.fastcsv.util.Preconditions;
import de.siegmar.fastcsv.util.Util;
/**
* This is the main class for writing CSV data.
*
* Example use:
* {@snippet :
* try (CsvWriter csv = CsvWriter.builder().build(file)) {
* csv.writeRecord("Hello", "world");
* }
*}
*/
@SuppressWarnings({"checkstyle:NPathComplexity", "checkstyle:CyclomaticComplexity"})
public final class CsvWriter implements Closeable, Flushable {
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 int currentLineNo = 1;
private final char[] lineDelimiterChars;
private final char[] emptyFieldValue;
private boolean openRecordWriter;
@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");
Preconditions.checkArgument(!containsDupe(fieldSeparator, quoteCharacter, commentCharacter),
"Control characters must differ (fieldSeparator=%s, quoteCharacter=%s, commentCharacter=%s)",
fieldSeparator, quoteCharacter, commentCharacter);
this.writer = writer;
this.fieldSeparator = fieldSeparator;
this.quoteCharacter = quoteCharacter;
this.commentCharacter = commentCharacter;
this.quoteStrategy = quoteStrategy;
this.lineDelimiter = Objects.requireNonNull(lineDelimiter);
emptyFieldValue = new char[] {quoteCharacter, quoteCharacter};
lineDelimiterChars = lineDelimiter.toString().toCharArray();
}
/**
* Creates a {@link CsvWriterBuilder} instance used to configure and create instances of
* this class.
*
* @return CsvWriterBuilder instance with default settings.
*/
public static CsvWriterBuilder builder() {
return new CsvWriterBuilder();
}
/**
* Writes a complete line - one or more fields and new line character(s) at the end.
*
* @param values the fields to write ({@code null} values are handled as empty strings, if
* not configured otherwise ({@link QuoteStrategies#EMPTY})).
* @return This CsvWriter.
* @throws UncheckedIOException if a write-error occurs
* @throws IllegalStateException if a record is already started (by calling {@link #writeRecord()}) and not ended
* @see #writeRecord(String...)
*/
public CsvWriter writeRecord(final Iterable values) {
validateNoOpenRecord();
try {
int fieldIdx = 0;
for (final String value : values) {
writeInternal(value, fieldIdx++);
}
return endRecord();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Writes a complete line - one or more fields and new line character(s) at the end.
*
* @param values the fields to write ({@code null} values are handled as empty strings, if
* not configured otherwise ({@link QuoteStrategies#EMPTY}))
* @return This CsvWriter.
* @throws UncheckedIOException if a write-error occurs
* @throws IllegalStateException if a record is already started (by calling {@link #writeRecord()}) and not ended
* @see #writeRecord(Iterable)
*/
public CsvWriter writeRecord(final String... values) {
validateNoOpenRecord();
try {
for (int i = 0; i < values.length; i++) {
writeInternal(values[i], i);
}
return endRecord();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Starts a new record.
*
* This method is used to write a record field by field. The record is ended by calling
* {@link CsvWriterRecord#endRecord()}.
*
* @return CsvWriterRecord instance to write fields to.
* @throws IllegalStateException if a record is already started
* @see #writeRecord(String...)
* @see #writeRecord(Iterable)
*/
public CsvWriterRecord writeRecord() {
validateNoOpenRecord();
openRecordWriter = true;
return new CsvWriterRecord();
}
private void validateNoOpenRecord() {
if (openRecordWriter) {
throw new IllegalStateException("Record already started, call end() on CsvWriterRecord first");
}
}
@SuppressWarnings("checkstyle:BooleanExpressionComplexity")
private void writeInternal(final String value, final int fieldIdx) throws IOException {
if (fieldIdx > 0) {
writer.write(fieldSeparator);
}
if (value == null) {
if (quoteStrategy != null && quoteStrategy.quoteNull(currentLineNo, fieldIdx)) {
writer.write(emptyFieldValue, 0, emptyFieldValue.length);
}
return;
}
final int length = value.length();
if (length == 0) {
if (quoteStrategy != null && quoteStrategy.quoteEmpty(currentLineNo, fieldIdx)) {
writer.write(emptyFieldValue, 0, emptyFieldValue.length);
}
return;
}
final boolean needsEscape = containsControlCharacter(value, fieldIdx, length);
final boolean needsQuotes = needsEscape
|| quoteStrategy != null && quoteStrategy.quoteNonEmpty(currentLineNo, fieldIdx, value);
if (needsQuotes) {
writer.write(quoteCharacter);
}
if (needsEscape) {
writeEscaped(writer, value, quoteCharacter);
} else {
writer.write(value, 0, length);
}
if (needsQuotes) {
writer.write(quoteCharacter);
}
}
@SuppressWarnings({
"checkstyle:BooleanExpressionComplexity",
"checkstyle:ReturnCount",
"checkstyle:MagicNumber",
"PMD.AvoidLiteralsInIfCondition"
})
private boolean containsControlCharacter(final String value, final int fieldIdx, final int length) {
if (fieldIdx == 0 && value.charAt(0) == commentCharacter) {
return true;
}
// For longer values, indexOf is faster than iterating over the string
if (length > 20) {
return value.indexOf(quoteCharacter) != -1
|| value.indexOf(fieldSeparator) != -1
|| value.indexOf(LF) != -1
|| value.indexOf(CR) != -1;
}
for (int i = 0; i < length; i++) {
final char c = value.charAt(i);
if (c == quoteCharacter || c == fieldSeparator || c == LF || c == CR) {
return true;
}
}
return false;
}
private static void writeEscaped(final Writable w, final String value, final char quoteChar)
throws IOException {
int startPos = 0;
int nextDelimPos = value.indexOf(quoteChar, startPos);
while (nextDelimPos != -1) {
// Write up to and including the delimiter
w.write(value, startPos, nextDelimPos - startPos + 1);
w.write(quoteChar);
startPos = nextDelimPos + 1;
nextDelimPos = value.indexOf(quoteChar, startPos);
}
// Write the rest of the string
w.write(value, startPos, value.length() - startPos);
}
/**
* Writes a comment line and new line character(s) at the end.
*
* Note that comments are not part of the CSV standard and may not be supported by all readers.
*
* @param comment the comment to write. The comment character
* (configured by {@link CsvWriterBuilder#commentCharacter(char)}) is automatically prepended.
* Empty or {@code null} values results in a line only consisting of the comment character.
* If the argument {@code comment} contains line break characters (CR, LF), multiple comment lines
* will be written, terminated with the line break character configured by
* {@link CsvWriterBuilder#lineDelimiter(LineDelimiter)}.
* @return This CsvWriter.
* @throws UncheckedIOException if a write-error occurs
* @throws IllegalStateException if a record is already started (by calling {@link #writeRecord()}) and not ended
*/
public CsvWriter writeComment(final String comment) {
validateNoOpenRecord();
try {
writer.write(commentCharacter);
if (comment != null && !comment.isEmpty()) {
writeCommentInternal(comment);
}
return endRecord();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
private void writeCommentInternal(final String comment) throws IOException {
final int length = comment.length();
int startPos = 0;
int lastChar = 0;
for (int i = 0; i < length; i++) {
final char c = comment.charAt(i);
if (c == CR) {
writeFragment(comment, i, startPos);
startPos = i + 1;
} else if (c == LF) {
if (lastChar != CR) {
writeFragment(comment, i, startPos);
}
startPos = i + 1;
}
lastChar = c;
}
if (length > startPos) {
writer.write(comment, startPos, length - startPos);
}
}
private void writeFragment(final String comment, final int i, final int startPos) throws IOException {
if (i > startPos) {
writer.write(comment, startPos, i - startPos);
}
writer.write(lineDelimiterChars, 0, lineDelimiterChars.length);
writer.write(commentCharacter);
}
private CsvWriter endRecord() throws IOException {
++currentLineNo;
writer.write(lineDelimiterChars, 0, lineDelimiterChars.length);
writer.endRecord();
return this;
}
@Override
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() + "[", "]")
.add("fieldSeparator=" + fieldSeparator)
.add("quoteCharacter=" + quoteCharacter)
.add("commentCharacter=" + commentCharacter)
.add("quoteStrategy=" + quoteStrategy)
.add("lineDelimiter='" + lineDelimiter + "'")
.toString();
}
/**
* This builder is used to create configured instances of {@link CsvWriter}. The default
* configuration of this class adheres with RFC 4180.
*
* - field separator: {@code ,} (comma)
* - quote character: {@code "} (double quote)
* - comment character: {@code #} (hash/number)
* - 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"})
public static final class CsvWriterBuilder {
private static final int DEFAULT_BUFFER_SIZE = 8192;
private char fieldSeparator = ',';
private char quoteCharacter = '"';
private char commentCharacter = '#';
private QuoteStrategy quoteStrategy;
private LineDelimiter lineDelimiter = LineDelimiter.CRLF;
private int bufferSize = DEFAULT_BUFFER_SIZE;
private boolean autoFlush;
CsvWriterBuilder() {
}
/**
* Sets the character that is used to separate fields – default: {@code ,} (comma).
*
* @param fieldSeparator the field separator character.
* @return This updated object, allowing additional method calls to be chained together.
*/
public CsvWriterBuilder fieldSeparator(final char fieldSeparator) {
this.fieldSeparator = fieldSeparator;
return this;
}
/**
* Sets the character used to quote values – default: {@code "} (double quote).
*
* Be aware that using characters other than the default double quote character goes against the RFC 4180
* standard.
*
* @param quoteCharacter the character for enclosing fields.
* @return This updated object, allowing additional method calls to be chained together.
*/
public CsvWriterBuilder quoteCharacter(final char quoteCharacter) {
this.quoteCharacter = quoteCharacter;
return this;
}
/**
* Sets the character used to prepend commented lines – default: {@code #} (hash/number).
*
* @param commentCharacter the character for prepending commented lines.
* @return This updated object, allowing additional method calls to be chained together.
*/
public CsvWriterBuilder commentCharacter(final char commentCharacter) {
this.commentCharacter = commentCharacter;
return this;
}
/**
* Sets the strategy that defines when optional quoting has to be performed – default: none.
*
* @param quoteStrategy the strategy when fields should be enclosed using the {@code quoteCharacter},
* even if not strictly required.
* @return This updated object, allowing additional method calls to be chained together.
*/
public CsvWriterBuilder quoteStrategy(final QuoteStrategy quoteStrategy) {
this.quoteStrategy = quoteStrategy;
return this;
}
/**
* Sets the delimiter used to separate lines (default: {@link LineDelimiter#CRLF}).
*
* @param lineDelimiter the line delimiter to be used.
* @return This updated object, allowing additional method calls to be chained together.
*/
public CsvWriterBuilder lineDelimiter(final LineDelimiter lineDelimiter) {
this.lineDelimiter = lineDelimiter;
return this;
}
/**
* Configures the size of the internal buffer.
*
* The default buffer size of 8,192 bytes usually does not need to be altered. One use-case is if you
* 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.
*/
public CsvWriterBuilder bufferSize(final int bufferSize) {
Preconditions.checkArgument(bufferSize >= 0, "buffer size must be >= 0");
this.bufferSize = 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.
*
* This library uses built-in buffering (unless {@link #bufferSize(int)} is used to disable it) but writes
* its internal buffer to the given {@code writer} at the end of every record write operation. Therefore,
* you probably want to pass in a {@link java.io.BufferedWriter} to retain good performance.
* Use {@link #build(Path, Charset, OpenOption...)} for optimal performance when writing files!
*
* @param writer the Writer to use for writing CSV data.
* @return a new CsvWriter instance - never {@code null}.
* @throws NullPointerException if writer is {@code null}
*/
public CsvWriter build(final Writer writer) {
Objects.requireNonNull(writer, "writer must not be null");
return csvWriter(writer, bufferSize, true, autoFlush);
}
/**
* Constructs a {@link CsvWriter} for the specified Path.
*
* @param file the file to write data to.
* @param openOptions options specifying how the file is opened.
* See {@link Files#newOutputStream(Path, OpenOption...)} for defaults.
* @return a new CsvWriter instance - never {@code null}. Remember to close it!
* @throws IOException if a write-error occurs
* @throws NullPointerException if file or charset is {@code null}
*/
public CsvWriter build(final Path file, final OpenOption... openOptions)
throws IOException {
return build(file, StandardCharsets.UTF_8, openOptions);
}
/**
* Constructs a {@link CsvWriter} for the specified Path.
*
* @param file the file to write data to.
* @param charset the character set to be used for writing data to the file.
* @param openOptions options specifying how the file is opened.
* See {@link Files#newOutputStream(Path, OpenOption...)} for defaults.
* @return a new CsvWriter instance - never {@code null}. Remember to close it!
* @throws IOException if a write-error occurs
* @throws NullPointerException if file or charset is {@code null}
*/
public CsvWriter build(final Path file, final Charset charset,
final OpenOption... openOptions)
throws IOException {
Objects.requireNonNull(file, "file must not be null");
Objects.requireNonNull(charset, "charset must not be null");
return csvWriter(new OutputStreamWriter(Files.newOutputStream(file, openOptions),
charset), bufferSize, false, autoFlush);
}
/**
* 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(writable,
fieldSeparator, quoteCharacter, commentCharacter, quoteStrategy, lineDelimiter);
}
@Override
public String toString() {
return new StringJoiner(", ", CsvWriterBuilder.class.getSimpleName() + "[", "]")
.add("fieldSeparator=" + fieldSeparator)
.add("quoteCharacter=" + quoteCharacter)
.add("commentCharacter=" + commentCharacter)
.add("quoteStrategy=" + quoteStrategy)
.add("lineDelimiter=" + lineDelimiter)
.add("bufferSize=" + bufferSize)
.add("autoFlush=" + autoFlush)
.toString();
}
}
/**
* This class is used to write a record field by field.
*
* The record is ended by calling {@link CsvWriterRecord#endRecord()}.
*/
public final class CsvWriterRecord {
private int fieldIdx;
private CsvWriterRecord() {
}
/**
* Writes a field to the current record.
* @param value the field value
* @return this CsvWriterRecord instance
* @throws UncheckedIOException if a write-error occurs
*/
public CsvWriterRecord writeField(final String value) {
try {
writeInternal(value, fieldIdx++);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
return this;
}
/**
* Ends the current record.
* @return the enclosing CsvWriter instance
* @throws UncheckedIOException if a write-error occurs
*/
public CsvWriter endRecord() {
openRecordWriter = false;
try {
return CsvWriter.this.endRecord();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
}
}