/* * Copyright 2007 Shawn Boyce. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * 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 net.techreadiness.plugin.action.reports; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.List; import java.util.SortedMap; /** * Writes output lines in CSV format. */ public class CSVWriter { /** Default comment character {@value} */ private static final char DEFAULT_COMMENT = '#'; /** Default field delimiter character {@value} */ private static final char DEFAULT_DELIMITER = ','; private static final char CARRIAGE_RETURN = '\r'; private static final char NEWLINE = '\n'; private static final char DOUBLE_QUOTE = '"'; private final Writer writer; private final char commentChar; private final char delimiter; private final String lineSeparator; /** * Constructor. Uses default comment and delimiter characters. * * @param writer * writer to output to */ public CSVWriter(Writer writer) { this(writer, DEFAULT_DELIMITER, DEFAULT_COMMENT); } /** * Constructor. * * @param writer * writer to output to * @param delimiter * field delimiter character to use */ public CSVWriter(Writer writer, char delimiter) { this(writer, delimiter, DEFAULT_COMMENT); } /** * Constructor. * * @param writer * writer to output to * @param delimiter * field delimiter character to use * @param comment * comment character to use */ public CSVWriter(Writer writer, char delimiter, char comment) { this.writer = writer; this.delimiter = delimiter; commentChar = comment; lineSeparator = System.getProperty("line.separator"); } /** * Output a CSV comment line. Comment lines start with the comment character. * <p> * Example: * <p> * <code> * writer.writeCommentLine( "this is a comment" ); * </code> * <p> * results in * * <pre> * # this is a comment line * </pre> * * @param comment * comment text to output. Embedded newline/carriage returns are handled correctly. * @throws IOException * if an error occurs when writing */ public void writeCommentLine(final String comment) throws IOException { // newlines output without modification in the comment will result in an // invalid CSV line // ensure that all comment lines are commented final String[] lines = comment.split("[\r\n]"); for (String line : lines) { writer.write(commentChar); writer.write(' '); writer.write(line); writer.write(lineSeparator); } } /** * Output the CSV header line which contains the names of the fields. This method should only be called once per file. * * @param fields * fields to output * @throws IOException * if an error occurs when writing */ public void writeHeaderLine(final SortedMap<String, String> fields) throws IOException { writeLine(fields.keySet()); } /** * Output the CSV data line which contains the field values * * @param fields * fields to output * @throws IOException * if an error occurs when writing */ public void writeDataLine(final SortedMap<String, String> fields) throws IOException { writeLine(fields.values()); } /** * Output a line of CSV fields. This can be field names or values. Each field is separated by the field delimiter. * * @param values * strings to output * @throws IOException * if an error occurs while writing */ public void writeLine(final List<String> values) throws IOException { writeLine((Collection<String>) values); } /** * Output a line of CSV fields. This can be field names or values. Each field is separated by the field delimiter. * * @param values * strings to output * @throws IOException * if an error occurs while writing */ public void writeLine(final String[] values) throws IOException { for (int ii = 0; ii < values.length; ii++) { escapeCSV(writer, values[ii], delimiter); if (ii + 1 < values.length) { writer.append(delimiter); } } writer.write(lineSeparator); } /** * Outputs a list of strings. * * @param values * strings to output * @throws IOException * if an I/O error occurs */ private void writeLine(final Collection<String> values) throws IOException { int ii = 0; for (String value : values) { escapeCSV(writer, value, delimiter); if (++ii < values.size()) { writer.append(delimiter); } } writer.write(lineSeparator); } /** * Escapes a text string for CSV output. * * @param text * text string to escape * @return appropriately escaped CSV text string */ public static String escapeCSV(final String text) { return escapeCSV(text, DEFAULT_DELIMITER); } /** * Escapes a text string for CSV output. * * @param text * text string to escape * @param delimiter * field delimiter * @return appropriately escaped CSV text string */ public static String escapeCSV(final String text, final char delimiter) { try { final StringWriter writer = new StringWriter(); escapeCSV(writer, text, delimiter); return writer.toString(); } catch (IOException e) { throw new RuntimeException("escapeCSV error", e); // should not // happen with a // StringWriter } } /** * Escapes a text string for CSV output. * * @param text * text string to escape * @param writer * writer to send the CSV output to * @param delimiter * field delimiter to use * @throws IOException * if an IO error occurs */ public static void escapeCSV(final Writer writer, final String text, char delimiter) throws IOException { final StringBuilder sbuf = new StringBuilder(); boolean isBuffering = true; // scan for special characters; if none, just output value for (int ii = 0; ii < text.length(); ii++) { final char ch = text.charAt(ii); switch (ch) { case DOUBLE_QUOTE: if (isBuffering) { isBuffering = false; writer.write(DOUBLE_QUOTE); if (sbuf.length() != 0) { writer.write(sbuf.toString()); } } // double quote is escaped with a second double quote writer.write(DOUBLE_QUOTE); writer.write(DOUBLE_QUOTE); break; // line feeds need to be escaped case CARRIAGE_RETURN: case NEWLINE: if (isBuffering) { isBuffering = false; writer.write(DOUBLE_QUOTE); if (sbuf.length() != 0) { writer.write(sbuf.toString()); } } writer.write(ch); break; default: if (ch == delimiter) { if (isBuffering) { isBuffering = false; writer.write(DOUBLE_QUOTE); if (sbuf.length() != 0) { writer.write(sbuf.toString()); } } writer.write(ch); } else if (isBuffering) { sbuf.append(ch); } else { writer.write(ch); } break; } } if (isBuffering) { writer.write(text); // no need for sbuf contents } else { writer.write(DOUBLE_QUOTE); } } }