/*******************************************************************************
* Copyright (c) 2010 Denis Solonenko.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v2.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* Denis Solonenko - initial API and implementation
******************************************************************************/
package ru.orangesoftware.financisto2.export.csv;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.Flushable;
import java.util.ArrayList;
import java.util.List;
/**
* These Csv.Reader and Csv.Writer implementations are based on the following description
* (since there is no formal specification of CSV format yet):
* - http://www.creativyst.com/Doc/Articles/CSV/CSV01.htm
* - http://tools.ietf.org/html/rfc4180
* - http://en.wikipedia.org/wiki/Comma-separated_values
* - http://www.csvreader.com/csv_format.php
*
* As far as CSV files are generally used by Excel, the default behavior of Csv.Reader and Csv.Writer
* corresponds to the Excel behaviour.
* In order to switch to the proper behaviour :) please use the following properties:
* - for Csv.Writer: delimiter will set the custom delimiter (default: semicolon)
* - for Csv.Reader: delimiter will set the custom delimiter (default: semicolon);
* preserveSpaces will preserve spaces from being trimmed (default: true);
* ignoreEmptyLines will ignore reading of empty lines (default: false);
* ignoreComments will ignore reading of comments (default: false).
*
* Examples of usage:
*
* Csv.Writer writer = new Csv.Writer("filename").delimiter(',');
* writer.comment("example of csv").value("a").value("b").newLine().value("c").close();
*
* A piece of code shown above will generate the following CSV file:
* #example of csv
* a,b
* c
*
* Csv.Reader reader = new Csv.Reader(new FileReader("filename")).delimiter(',').ignoreComments(true);
* System.out.println(reader.readLine());
*
* A piece of code shown above will print to console the following text (using the CSV file generated earlier):
* [a, b]
*
* @author Konstantin Chapyuk aka c0nst
* @url http://www.javenue.info
* @version 1.0
* @see javenue.csv.CsvTestCase
*/
public class Csv {
public static class Writer {
private Appendable appendable;
private char delimiter = ';';
private boolean first = true;
public Writer(String fileName) { this(new File(fileName)); }
public Writer(File file) {
try {
appendable = new FileWriter(file);
} catch (java.io.IOException e) { throw new IOException(e); }
}
public Writer(Appendable appendable) { this.appendable = appendable; }
public Writer value(String value) {
if (!first) string("" + delimiter);
string(escape(value));
first = false;
return this;
}
public Writer newLine() {
first = true;
return string("\n");
}
public Writer comment(String comment) {
if (!first) throw new FormatException("invalid csv: misplaced comment");
return string("#").string(comment).newLine();
}
public Writer flush() {
try {
if (appendable instanceof Flushable) {
Flushable flushable = (Flushable) appendable;
flushable.flush();
}
} catch (java.io.IOException e) { throw new IOException(e); }
return this;
}
public void close() {
try {
if (appendable instanceof Closeable) {
Closeable closeable = (Closeable) appendable;
closeable.close();
}
} catch (java.io.IOException e) { throw new IOException(e); }
}
private Writer string(String s) {
try {
appendable.append(s);
} catch (java.io.IOException e) { throw new IOException(e); }
return this;
}
private String escape(String value) {
if (value == null) return "";
if (value.length() == 0) return "\"\"";
boolean needQuoting = value.startsWith(" ") || value.endsWith(" ") || (value.startsWith("#") && first);
if (!needQuoting) {
for (char ch : new char[]{'\"', '\\', '\r', '\n', '\t', delimiter}) {
if (value.indexOf(ch) != -1) {
needQuoting = true;
break;
}
}
}
String result = value.replace("\"", "\"\"");
if (needQuoting) result = "\"" + result + "\"";
return result;
}
public Writer delimiter(char delimiter) { this.delimiter = delimiter; return this; }
}
public static class Reader {
private static final String impossibleString = "$#%^&*!xyxb$#%&*!^";
private BufferedReader reader;
private char delimiter = ';';
private boolean preserveSpaces = true;
private boolean ignoreEmptyLines = false;
private boolean ignoreComments = false;
public Reader(java.io.Reader reader) { this.reader = new BufferedReader(reader); }
public List<String> readLine() {
String line;
try {
line = reader.readLine();
} catch (java.io.IOException e) { throw new IOException(e); }
if (line == null) return null;
if (!preserveSpaces) line = removeLeadingSpaces(line);
if (ignoreComments && line.startsWith("#")) return readLine();
if (ignoreEmptyLines && line.length() == 0) return readLine();
List<String> result = new ArrayList<String>();
while (line != null) {
String token = "";
int nextDelimiterIndex = line.indexOf(delimiter);
int openQuoteIndex = line.indexOf("\"");
if ((nextDelimiterIndex > openQuoteIndex || nextDelimiterIndex == -1) && openQuoteIndex != -1) {
token = line.substring(0, openQuoteIndex + 1);
line = markDoubleQuotes(line.substring(openQuoteIndex + 1));
int closeQuoteIndex = line.indexOf("\"");
while (closeQuoteIndex == -1) {
token += line + "\n";
try {
line = reader.readLine();
} catch (java.io.IOException e) { throw new IOException(e); }
if (line == null) throw new FormatException("invalid csv: premature end of csv");
closeQuoteIndex = line.indexOf("\"");
}
nextDelimiterIndex = line.indexOf(delimiter, closeQuoteIndex);
}
if (nextDelimiterIndex == -1) {
token += line;
line = null;
} else {
token += line.substring(0, nextDelimiterIndex);
line = unmarkDoubleQuotes(line.substring(nextDelimiterIndex + 1, line.length()));
}
result.add(unescape(token));
}
return result;
}
public void close() {
try {
reader.close();
} catch (java.io.IOException e) { throw new IOException(e); }
}
private String unescape(String s) {
String result = s;
if (!preserveSpaces || result.contains("\"")) result = result.trim();
if (result.startsWith("\"") ^ result.endsWith("\"")) throw new FormatException("invalid csv: misplaced quote");
if (result.startsWith("\"")) result = result.substring(1, result.length() - 1);
result = markDoubleQuotes(result);
if (result.contains("\"")) throw new FormatException("invalid csv: misplaced quote"); // could this ever happen at all?
result = unmarkDoubleQuotes(result);
return result;
}
private String unmarkDoubleQuotes(String s) { return s.replace(impossibleString, "\"\""); }
private String markDoubleQuotes(String s) { return s.replace("\"\"", impossibleString); }
private String removeLeadingSpaces(String s) { return s.replaceFirst(" +", ""); }
public Reader delimiter(char delimiter) { this.delimiter = delimiter; return this; }
public Reader preserveSpaces(boolean preserveSpaces) { this.preserveSpaces = preserveSpaces; return this; }
public Reader ignoreEmptyLines(boolean ignoreEmptyLines) { this.ignoreEmptyLines = ignoreEmptyLines; return this; }
public Reader ignoreComments(boolean ignoreComments) { this.ignoreComments = ignoreComments; return this; }
}
public static class Exception extends RuntimeException {
private static final long serialVersionUID = 1L;
public Exception() { }
public Exception(String message) { super(message); }
public Exception(String message, Throwable cause) { super(message, cause); }
public Exception(Throwable cause) { super(cause); }
}
public static class IOException extends Exception {
private static final long serialVersionUID = 1L;
public IOException() { }
public IOException(String message) { super(message); }
public IOException(String message, Throwable cause) { super(message, cause); }
public IOException(Throwable cause) { super(cause); }
}
public static class FormatException extends Exception {
private static final long serialVersionUID = 1L;
public FormatException() { }
public FormatException(String message) { super(message); }
public FormatException(String message, Throwable cause) { super(message, cause); }
public FormatException(Throwable cause) { super(cause); }
}
}