org.apache.commons.csv.CSVPrinter Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.commons.csv;
import static org.apache.commons.csv.Constants.COMMENT;
import static org.apache.commons.csv.Constants.CR;
import static org.apache.commons.csv.Constants.LF;
import static org.apache.commons.csv.Constants.SP;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Prints values in a CSV format.
*
* @version $Id: CSVPrinter.java 1617076 2014-08-10 09:23:01Z britter $
*/
public final class CSVPrinter implements Flushable, Closeable {
/** The place that the values get written. */
private final Appendable out;
private final CSVFormat format;
/** True if we just began a new record. */
private boolean newRecord = true;
/**
* Creates a printer that will print values to the given stream following the CSVFormat.
*
* Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
* and escaping with a different character) are not supported.
*
*
* @param out
* stream to which to print. Must not be null.
* @param format
* the CSV format. Must not be null.
* @throws IOException
* thrown if the optional header cannot be printed.
* @throws IllegalArgumentException
* thrown if the parameters of the format are inconsistent or if either out or format are null.
*/
public CSVPrinter(final Appendable out, final CSVFormat format) throws IOException {
Assertions.notNull(out, "out");
Assertions.notNull(format, "format");
this.out = out;
this.format = format;
// TODO: Is it a good idea to do this here instead of on the first call to a print method?
// It seems a pain to have to track whether the header has already been printed or not.
if (format.getHeader() != null) {
this.printRecord((Object[]) format.getHeader());
}
}
// ======================================================
// printing implementation
// ======================================================
public void close() throws IOException {
if (out instanceof Closeable) {
((Closeable) out).close();
}
}
/**
* Flushes the underlying stream.
*
* @throws IOException
* If an I/O error occurs
*/
public void flush() throws IOException {
if (out instanceof Flushable) {
((Flushable) out).flush();
}
}
/**
* Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
*
* @param value
* value to be output.
* @throws IOException
* If an I/O error occurs
*/
public void print(final Object value) throws IOException {
// null values are considered empty
String strValue;
if (value == null) {
final String nullString = format.getNullString();
strValue = nullString == null ? Constants.EMPTY : nullString;
} else {
strValue = value.toString();
}
this.print(value, strValue, 0, strValue.length());
}
private void print(final Object object, final CharSequence value,
final int offset, final int len) throws IOException {
if (!newRecord) {
out.append(format.getDelimiter());
}
if (format.isQuoteCharacterSet()) {
// the original object is needed so can check for Number
printAndQuote(object, value, offset, len);
} else if (format.isEscapeCharacterSet()) {
printAndEscape(value, offset, len);
} else {
out.append(value, offset, offset + len);
}
newRecord = false;
}
/*
* Note: must only be called if escaping is enabled, otherwise will generate NPE
*/
private void printAndEscape(final CharSequence value, final int offset, final int len) throws IOException {
int start = offset;
int pos = offset;
final int end = offset + len;
final char delim = format.getDelimiter();
final char escape = format.getEscapeCharacter().charValue();
while (pos < end) {
char c = value.charAt(pos);
if (c == CR || c == LF || c == delim || c == escape) {
// write out segment up until this char
if (pos > start) {
out.append(value, start, pos);
}
if (c == LF) {
c = 'n';
} else if (c == CR) {
c = 'r';
}
out.append(escape);
out.append(c);
start = pos + 1; // start on the current char after this one
}
pos++;
}
// write last segment
if (pos > start) {
out.append(value, start, pos);
}
}
/*
* Note: must only be called if quoting is enabled, otherwise will generate NPE
*/
// the original object is needed so can check for Number
private void printAndQuote(final Object object, final CharSequence value,
final int offset, final int len) throws IOException {
boolean quote = false;
int start = offset;
int pos = offset;
final int end = offset + len;
final char delimChar = format.getDelimiter();
final char quoteChar = format.getQuoteCharacter().charValue();
QuoteMode quoteModePolicy = format.getQuoteMode();
if (quoteModePolicy == null) {
quoteModePolicy = QuoteMode.MINIMAL;
}
switch (quoteModePolicy) {
case ALL:
quote = true;
break;
case NON_NUMERIC:
quote = !(object instanceof Number);
break;
case NONE:
// Use the existing escaping code
printAndEscape(value, offset, len);
return;
case MINIMAL:
if (len <= 0) {
// always quote an empty token that is the first
// on the line, as it may be the only thing on the
// line. If it were not quoted in that case,
// an empty line has no tokens.
if (newRecord) {
quote = true;
}
} else {
char c = value.charAt(pos);
// TODO where did this rule come from?
if (newRecord && (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z'))) {
quote = true;
} else if (c <= COMMENT) {
// Some other chars at the start of a value caused the parser to fail, so for now
// encapsulate if we start in anything less than '#'. We are being conservative
// by including the default comment char too.
quote = true;
} else {
while (pos < end) {
c = value.charAt(pos);
if (c == LF || c == CR || c == quoteChar || c == delimChar) {
quote = true;
break;
}
pos++;
}
if (!quote) {
pos = end - 1;
c = value.charAt(pos);
// Some other chars at the end caused the parser to fail, so for now
// encapsulate if we end in anything less than ' '
if (c <= SP) {
quote = true;
}
}
}
}
if (!quote) {
// no encapsulation needed - write out the original value
out.append(value, start, end);
return;
}
break;
default:
throw new IllegalStateException("Unexpected Quote value: " + quoteModePolicy);
}
if (!quote) {
// no encapsulation needed - write out the original value
out.append(value, start, end);
return;
}
// we hit something that needed encapsulation
out.append(quoteChar);
// Pick up where we left off: pos should be positioned on the first character that caused
// the need for encapsulation.
while (pos < end) {
final char c = value.charAt(pos);
if (c == quoteChar) {
// write out the chunk up until this point
// add 1 to the length to write out the encapsulator also
out.append(value, start, pos + 1);
// put the next starting position on the encapsulator so we will
// write it out again with the next string (effectively doubling it)
start = pos;
}
pos++;
}
// write the last segment
out.append(value, start, pos);
out.append(quoteChar);
}
/**
* Prints a comment on a new line among the delimiter separated values.
*
*
* Comments will always begin on a new line and occupy a least one full line. The character specified to start
* comments and a space will be inserted at the beginning of each new line in the comment.
*
*
* If comments are disabled in the current CSV format this method does nothing.
*
* @param comment
* the comment to output
* @throws IOException
* If an I/O error occurs
*/
public void printComment(final String comment) throws IOException {
if (!format.isCommentMarkerSet()) {
return;
}
if (!newRecord) {
println();
}
out.append(format.getCommentMarker().charValue());
out.append(SP);
for (int i = 0; i < comment.length(); i++) {
final char c = comment.charAt(i);
switch (c) {
case CR:
if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
i++;
}
//$FALL-THROUGH$ break intentionally excluded.
case LF:
println();
out.append(format.getCommentMarker().charValue());
out.append(SP);
break;
default:
out.append(c);
break;
}
}
println();
}
/**
* Outputs the record separator.
*
* @throws IOException
* If an I/O error occurs
*/
public void println() throws IOException {
final String recordSeparator = format.getRecordSeparator();
if (recordSeparator != null) {
out.append(recordSeparator);
}
newRecord = true;
}
/**
* Prints the given values a single record of delimiter separated values followed by the record separator.
*
*
* The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
* separator to the output after printing the record, so there is no need to call {@link #println()}.
*
*
* @param values
* values to output.
* @throws IOException
* If an I/O error occurs
*/
public void printRecord(final Iterable> values) throws IOException {
for (final Object value : values) {
print(value);
}
println();
}
/**
* Prints the given values a single record of delimiter separated values followed by the record separator.
*
*
* The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
* separator to the output after printing the record, so there is no need to call {@link #println()}.
*
*
* @param values
* values to output.
* @throws IOException
* If an I/O error occurs
*/
public void printRecord(final Object... values) throws IOException {
for (final Object value : values) {
print(value);
}
println();
}
/**
* Prints all the objects in the given collection handling nested collections/arrays as records.
*
* If the given collection only contains simple objects, this method will print a single record like
* {@link #printRecord(Iterable)}. If the given collections contains nested collections/arrays those nested elements
* will each be printed as records using {@link #printRecord(Object...)}.
*
* Given the following data structure:
*
*
* List<String[]> data = ...
* data.add(new String[]{ "A", "B", "C" });
* data.add(new String[]{ "1", "2", "3" });
* data.add(new String[]{ "A1", "B2", "C3" });
*
*
*
* Calling this method will print:
*
*
* A, B, C
* 1, 2, 3
* A1, B2, C3
*
*
*
* @param values
* the values to print.
* @throws IOException
* If an I/O error occurs
*/
public void printRecords(final Iterable> values) throws IOException {
for (final Object value : values) {
if (value instanceof Object[]) {
this.printRecord((Object[]) value);
} else if (value instanceof Iterable) {
this.printRecord((Iterable>) value);
} else {
this.printRecord(value);
}
}
}
/**
* Prints all the objects in the given array handling nested collections/arrays as records.
*
* If the given array only contains simple objects, this method will print a single record like
* {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
* elements will each be printed as records using {@link #printRecord(Object...)}.
*
* Given the following data structure:
*
*
* String[][] data = new String[3][]
* data[0] = String[]{ "A", "B", "C" };
* data[1] = new String[]{ "1", "2", "3" };
* data[2] = new String[]{ "A1", "B2", "C3" };
*
*
*
* Calling this method will print:
*
*
* A, B, C
* 1, 2, 3
* A1, B2, C3
*
*
*
* @param values
* the values to print.
* @throws IOException
* If an I/O error occurs
*/
public void printRecords(final Object... values) throws IOException {
for (final Object value : values) {
if (value instanceof Object[]) {
this.printRecord((Object[]) value);
} else if (value instanceof Iterable) {
this.printRecord((Iterable>) value);
} else {
this.printRecord(value);
}
}
}
/**
* Prints all the objects in the given JDBC result set.
*
* @param resultSet result set
* the values to print.
* @throws IOException
* If an I/O error occurs
* @throws SQLException if a database access error occurs
*/
public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
final int columnCount = resultSet.getMetaData().getColumnCount();
while (resultSet.next()) {
for (int i = 1; i <= columnCount; i++) {
print(resultSet.getString(i));
}
println();
}
}
/**
* Gets the target Appendable.
*
* @return the target Appendable.
*/
public Appendable getOut() {
return this.out;
}
}