package winterwell.utils.io;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import winterwell.utils.IORException;
import winterwell.utils.StrUtils;
import winterwell.utils.Utils;
/**
* Support for creating .csv files.
*
* Implements "standard" CSV behaviour as per
* http://en.wikipedia.org/wiki/Comma-separated_values
*
* TODO is this thread safe?? depends on whether {@link BufferedWriter} is. if
* needed maybe use a lock-free queue instead of a lock for high concurrency??
*
* TODO: Add method for writing a comment, quote fields containing comment chars
* ?
*
* @author daniel
* @testedby {@link CSVWriterTest}
*/
public class CSVWriter implements Closeable {
private static BufferedWriter CSVWriter2_fileWriter(File file,
boolean append) {
try {
return FileUtils.getWriter(new FileOutputStream(file, append));
} catch (FileNotFoundException e) {
throw Utils.runtime(e);
}
}
char commentMarker;
private final char delimiter;
private File file;
private CharSequence LINEEND = StrUtils.LINEEND;
int linesWritten = 0;
private BufferedWriter out;
private final String quote; // This should be ' or "
private final String quotedQuote;
/**
* Create a CSV file with the standard double-quote quote character.
*
* @param file
* @param delimiter
* @throws FileNotFoundException
*/
public CSVWriter(File file, char delimiter) {
this(file, delimiter, '"', false);
}
/**
* Work with CSV file with the standard double-quote quote character.
*
* @param file
* @param delimiter
* @throws FileNotFoundException
*/
public CSVWriter(File file, char delimiter, boolean append) {
this(file, delimiter, '"', append);
}
public CSVWriter(File file, char delimiter, char quote)
throws FileNotFoundException {
this(file, delimiter, quote, false);
}
public CSVWriter(File file, char delimiter, char quote, boolean append) {
this(CSVWriter2_fileWriter(file, append), delimiter, quote);
this.file = file;
}
public CSVWriter(Writer out, char delimiter, char quote) {
assert out != null;
file = null;
this.out = out instanceof BufferedWriter ? (BufferedWriter) out
: new BufferedWriter(out);
this.delimiter = delimiter;
// Possibly this is too restrictive, but actually other values don't
// really make sense
assert quote == '\'' || quote == '"';
this.quote = "" + quote;
this.quotedQuote = "" + quote + quote;
}
/**
* Flush & close the underlying file writer
*/
@Override
public void close() {
try { // is this needed?
flush();
} catch (Exception e) {
// oh well;
}
FileUtils.close(out);
}
public void flush() {
try {
out.flush();
} catch (IOException e) {
throw new IORException(e);
}
}
/**
* @return file (if created using the file constructor) or null. null does
* not imply that this is not a file-based writer.
*/
public File getFile() {
return file;
}
/**
* Set this writer to append to the end of an existing file. Must be called
* before any lines are written
*
* @param append
*/
public void setAppend(boolean append) {
assert linesWritten == 0;
if (!append)
return;
try {
out = FileUtils.getWriter(new FileOutputStream(file, true));
} catch (FileNotFoundException e) {
throw new IORException(e);
}
}
/**
* @param commentMarker
* If set (eg to '#'), then items beginning with this character
* will be quoted to avoid them being interpreted as comments at
* the other end. 0 by default. Comment markers are not standard
* csv to the extent that there is such a thing.
*/
public void setCommentMarker(char commentMarker) {
this.commentMarker = commentMarker;
}
/**
* Change the default line-end. E.g. if you want to force M$ style \r\n
* output
*
* @param lineEnd
*/
public void setLineEnd(CharSequence lineEnd) {
LINEEND = lineEnd;
}
@Override
public String toString() {
return file == null ? getClass().getSimpleName() : getClass()
.getSimpleName() + "[" + file + "]";
}
/**
* Convenience for {@link #write(Object[])}
*
* @param line
*/
public void write(List line) {
write(line.toArray());
}
/**
* Write out a row.
*
* @param objects
* These will be converted by {@link String#valueOf(Object)},
* with escaping of the delimiter and the escape char. Quotes:
* added if set, otherwise line-breaks are converted into spaces.
*/
public void write(Object... strings) {
// defend against accidentally routing to the wrong method
if (strings.length == 1 && strings[0] instanceof String[]) {
write((String[]) strings[0]);
return;
}
String[] ss = new String[strings.length];
for (int i = 0; i < strings.length; i++) {
ss[i] = strings[i] == null ? null : String.valueOf(strings[i]);
}
write(ss);
}
/**
* Write out a row.
*
* @param objects
* These will be escaping for the delimiter and the escape char.
* Quotes: added if set, otherwise line-breaks are converted into
* spaces.
*/
public void write(String... strings) {
linesWritten++;
try {
if (strings.length == 0) {
out.append(LINEEND);
return;
}
StringBuilder sb = new StringBuilder();
for (int i = 0, n = strings.length; i < n; i++) {
String si = strings[i] == null ? "" : strings[i];
// TODO: Add an option to suppress in-field line breaks
// NB: Line breaking within a quote is okay per the standard
// If field contains the delimiter, quote-char, newline, or
// comment-char it must be quoted
if (si.indexOf(delimiter) != -1
|| si.indexOf(quote) != -1
|| si.indexOf('\n') != -1
|| (commentMarker != 0 && si.indexOf(commentMarker) != -1)) {
// Quote character must be replaced by double quote
si = si.replace(quote, quotedQuote);
si = quote + si + quote;
}
sb.append(si);
sb.append(delimiter);
}
// remove final delimiter
StrUtils.pop(sb, 1);
sb.append(LINEEND);
// write
out.append(sb);
} catch (IOException ex) {
throw new IORException(ex);
}
}
public void writeComment(String comment) {
if (commentMarker == 0)
throw new IllegalStateException(
"You must specify a comment marker before writing comments");
// ??
if (comment.charAt(0) == commentMarker) {
comment = comment.substring(1);
}
try {
out.append(commentMarker);
out.append(' ');
out.append(comment);
out.append(LINEEND);
} catch (IOException e) {
throw Utils.runtime(e);
}
}
}