001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: CSVOutput.java 308 2012-03-10 22:26:06Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.string;
009
010 import au.com.bytecode.opencsv.CSVWriter;
011
012 import java.io.BufferedWriter;
013 import java.io.IOException;
014 import java.io.Writer;
015 import java.util.Arrays;
016 import java.util.Date;
017 import java.util.HashSet;
018 import java.util.LinkedHashSet;
019 import java.util.Map;
020
021 /**
022 * CSV file output stream that ensures values are matched to the correct columns.
023 * This class requires the <a href="http://opencsv.sourceforge.net/">OpenCSV</a> library.
024 *
025 * @see <a href="http://opencsv.sourceforge.net/">OpenCSV</a>
026 */
027 public class CSVOutput {
028
029 private final CSVWriter writer;
030 private final String[] columns;
031
032 /**
033 * Constructor.
034 *
035 * @param writer destination for the CSV output
036 * @param columns CSV columns names in their desired order
037 * @throws IllegalArgumentException if {@code writer} is null
038 * @throws IllegalArgumentException if {@code columns} is null
039 * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
040 */
041 public CSVOutput(Writer writer, String... columns) {
042 this(writer, Arrays.asList(columns));
043 }
044
045 /**
046 * Constructor.
047 *
048 * @param writer destination for the CSV output
049 * @param columns CSV columns names, iterated in their desired order
050 * @throws IllegalArgumentException if {@code writer} is null
051 * @throws IllegalArgumentException if {@code columns} is null
052 * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
053 */
054 public CSVOutput(Writer writer, Iterable<String> columns) {
055 this(new CSVWriter(new BufferedWriter(writer)), columns);
056 if (writer == null)
057 throw new IllegalArgumentException("null writer");
058 }
059
060 /**
061 * Constructor.
062 *
063 * @param writer CSV output object
064 * @param columns CSV columns names, iterated in their desired order
065 * @throws IllegalArgumentException if {@code writer} is null
066 * @throws IllegalArgumentException if {@code columns} is null
067 * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
068 */
069 public CSVOutput(CSVWriter writer, Iterable<String> columns) {
070 if (writer == null)
071 throw new IllegalArgumentException("null writer");
072 if (columns == null)
073 throw new IllegalArgumentException("null columns");
074 this.writer = writer;
075 LinkedHashSet<String> columnSet = new LinkedHashSet<String>();
076 for (String column : columns) {
077 if (!columnSet.add(column))
078 throw new IllegalArgumentException("duplicate column name `" + column + "'");
079 }
080 this.columns = columnSet.toArray(new String[columnSet.size()]);
081
082 // Output header
083 this.writer.writeNext(this.columns);
084 }
085
086 /**
087 * Output a CSV row.
088 *
089 * @param row mapping from column name to value; missing or values are treated as null
090 * @throws IllegalArgumentException if {@code row} contains an unknown column name
091 */
092 public void writeRow(Map<String, ?> row) {
093
094 // Sanity check column names
095 HashSet<String> unknowns = new HashSet<String>(row.keySet());
096 unknowns.removeAll(Arrays.asList(this.columns));
097 if (!unknowns.isEmpty())
098 throw new IllegalArgumentException("row contains unknown column(s): " + unknowns);
099
100 // Format columns
101 String[] values = new String[this.columns.length];
102 for (int i = 0; i < this.columns.length; i++)
103 values[i] = this.formatObject(this.columns[i], row.get(this.columns[i]));
104
105 // Output row
106 this.writer.writeNext(values);
107 }
108
109 /**
110 * Flush output.
111 */
112 public void flush() throws IOException {
113 this.writer.flush();
114 }
115
116 /**
117 * Close this instance and the underlying output.
118 */
119 public void close() throws IOException {
120 this.writer.close();
121 }
122
123 /**
124 * Format a CSV column value.
125 *
126 * <p>
127 * The implementation in {@link CSVOutput} applies the following logic:
128 * <ul>
129 * <li>{@code null} values are output as the empty string</li>
130 * <li>{@link Boolean} values are output as {@code 1} or {@code 0}</li>
131 * <li>{@link Date} values are output by delegating to {@link #formatDate formatDate()}</li>
132 * <li>All other objects are output using {@link String#valueOf}</li>
133 * </ul>
134 * </p>
135 *
136 * <p>
137 * Subclasses should override as needed.
138 * </p>
139 *
140 * @param columnName name of the column
141 * @param value column value; will be null if no value was present in the {@link Map} parameter to {@link #writeRow}
142 */
143 protected String formatObject(String columnName, Object value) {
144 if (value == null)
145 return "";
146 if (value instanceof Boolean)
147 return ((Boolean)value).booleanValue() ? "1" : "0";
148 if (value instanceof Date)
149 return this.formatDate(columnName, (Date)value);
150 return String.valueOf(value);
151 }
152
153 /**
154 * Format a {@link Date} value.
155 *
156 * <p>
157 * The implementation in {@link CSVOutput} delegates to {@link DateEncoder#encode DateEncoder.encode()}.
158 * </p>
159 *
160 * <p>
161 * Subclasses should override as needed.
162 * </p>
163 *
164 * @param columnName name of the column
165 * @param date column value
166 * @throws IllegalArgumentException if {@code date} is null
167 */
168 protected String formatDate(String columnName, Date date) {
169 if (date == null)
170 throw new IllegalArgumentException("null date");
171 return DateEncoder.encode(date);
172 }
173 }
174