001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.csv;
019
020import static org.apache.commons.csv.Constants.CR;
021import static org.apache.commons.csv.Constants.LF;
022import static org.apache.commons.csv.Constants.SP;
023
024import java.io.Closeable;
025import java.io.Flushable;
026import java.io.IOException;
027import java.sql.ResultSet;
028import java.sql.SQLException;
029
030/**
031 * Prints values in a CSV format.
032 */
033public final class CSVPrinter implements Flushable, Closeable {
034
035    /** The place that the values get written. */
036    private final Appendable out;
037    private final CSVFormat format;
038
039    /** True if we just began a new record. */
040    private boolean newRecord = true;
041
042    /**
043     * Creates a printer that will print values to the given stream following the CSVFormat.
044     * <p>
045     * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
046     * and escaping with a different character) are not supported.
047     * </p>
048     *
049     * @param out
050     *            stream to which to print. Must not be null.
051     * @param format
052     *            the CSV format. Must not be null.
053     * @throws IOException
054     *             thrown if the optional header cannot be printed.
055     * @throws IllegalArgumentException
056     *             thrown if the parameters of the format are inconsistent or if either out or format are null.
057     */
058    public CSVPrinter(final Appendable out, final CSVFormat format) throws IOException {
059        Assertions.notNull(out, "out");
060        Assertions.notNull(format, "format");
061
062        this.out = out;
063        this.format = format;
064        // TODO: Is it a good idea to do this here instead of on the first call to a print method?
065        // It seems a pain to have to track whether the header has already been printed or not.
066        if (format.getHeaderComments() != null) {
067            for (final String line : format.getHeaderComments()) {
068                if (line != null) {
069                    this.printComment(line);
070                }
071            }
072        }
073        if (format.getHeader() != null && !format.getSkipHeaderRecord()) {
074            this.printRecord((Object[]) format.getHeader());
075        }
076    }
077
078    // ======================================================
079    // printing implementation
080    // ======================================================
081
082    @Override
083    public void close() throws IOException {
084        if (out instanceof Closeable) {
085            ((Closeable) out).close();
086        }
087    }
088
089    /**
090     * Flushes the underlying stream.
091     *
092     * @throws IOException
093     *             If an I/O error occurs
094     */
095    @Override
096    public void flush() throws IOException {
097        if (out instanceof Flushable) {
098            ((Flushable) out).flush();
099        }
100    }
101
102    /**
103     * Gets the target Appendable.
104     *
105     * @return the target Appendable.
106     */
107    public Appendable getOut() {
108        return this.out;
109    }
110
111    /**
112     * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
113     *
114     * @param value
115     *            value to be output.
116     * @throws IOException
117     *             If an I/O error occurs
118     */
119    public void print(final Object value) throws IOException {
120        format.print(value, out, newRecord);
121        newRecord = false;
122    }
123
124    /**
125     * Prints a comment on a new line among the delimiter separated values.
126     *
127     * <p>
128     * Comments will always begin on a new line and occupy a least one full line. The character specified to start
129     * comments and a space will be inserted at the beginning of each new line in the comment.
130     * </p>
131     *
132     * If comments are disabled in the current CSV format this method does nothing.
133     *
134     * @param comment
135     *            the comment to output
136     * @throws IOException
137     *             If an I/O error occurs
138     */
139    public void printComment(final String comment) throws IOException {
140        if (!format.isCommentMarkerSet()) {
141            return;
142        }
143        if (!newRecord) {
144            println();
145        }
146        out.append(format.getCommentMarker().charValue());
147        out.append(SP);
148        for (int i = 0; i < comment.length(); i++) {
149            final char c = comment.charAt(i);
150            switch (c) {
151            case CR:
152                if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
153                    i++;
154                }
155                //$FALL-THROUGH$ break intentionally excluded.
156            case LF:
157                println();
158                out.append(format.getCommentMarker().charValue());
159                out.append(SP);
160                break;
161            default:
162                out.append(c);
163                break;
164            }
165        }
166        println();
167    }
168
169    /**
170     * Outputs the record separator.
171     *
172     * @throws IOException
173     *             If an I/O error occurs
174     */
175    public void println() throws IOException {
176        format.println(out);
177        newRecord = true;
178    }
179
180    /**
181     * Prints the given values a single record of delimiter separated values followed by the record separator.
182     *
183     * <p>
184     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
185     * separator to the output after printing the record, so there is no need to call {@link #println()}.
186     * </p>
187     *
188     * @param values
189     *            values to output.
190     * @throws IOException
191     *             If an I/O error occurs
192     */
193    public void printRecord(final Iterable<?> values) throws IOException {
194        for (final Object value : values) {
195            print(value);
196        }
197        println();
198    }
199
200    /**
201     * Prints the given values a single record of delimiter separated values followed by the record separator.
202     *
203     * <p>
204     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
205     * separator to the output after printing the record, so there is no need to call {@link #println()}.
206     * </p>
207     *
208     * @param values
209     *            values to output.
210     * @throws IOException
211     *             If an I/O error occurs
212     */
213    public void printRecord(final Object... values) throws IOException {
214        format.printRecord(out, values);
215        newRecord = true;
216    }
217
218    /**
219     * Prints all the objects in the given collection handling nested collections/arrays as records.
220     *
221     * <p>
222     * If the given collection only contains simple objects, this method will print a single record like
223     * {@link #printRecord(Iterable)}. If the given collections contains nested collections/arrays those nested elements
224     * will each be printed as records using {@link #printRecord(Object...)}.
225     * </p>
226     *
227     * <p>
228     * Given the following data structure:
229     * </p>
230     *
231     * <pre>
232     * <code>
233     * List&lt;String[]&gt; data = ...
234     * data.add(new String[]{ "A", "B", "C" });
235     * data.add(new String[]{ "1", "2", "3" });
236     * data.add(new String[]{ "A1", "B2", "C3" });
237     * </code>
238     * </pre>
239     *
240     * <p>
241     * Calling this method will print:
242     * </p>
243     *
244     * <pre>
245     * <code>
246     * A, B, C
247     * 1, 2, 3
248     * A1, B2, C3
249     * </code>
250     * </pre>
251     *
252     * @param values
253     *            the values to print.
254     * @throws IOException
255     *             If an I/O error occurs
256     */
257    public void printRecords(final Iterable<?> values) throws IOException {
258        for (final Object value : values) {
259            if (value instanceof Object[]) {
260                this.printRecord((Object[]) value);
261            } else if (value instanceof Iterable) {
262                this.printRecord((Iterable<?>) value);
263            } else {
264                this.printRecord(value);
265            }
266        }
267    }
268
269    /**
270     * Prints all the objects in the given array handling nested collections/arrays as records.
271     *
272     * <p>
273     * If the given array only contains simple objects, this method will print a single record like
274     * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
275     * elements will each be printed as records using {@link #printRecord(Object...)}.
276     * </p>
277     *
278     * <p>
279     * Given the following data structure:
280     * </p>
281     *
282     * <pre>
283     * <code>
284     * String[][] data = new String[3][]
285     * data[0] = String[]{ "A", "B", "C" };
286     * data[1] = new String[]{ "1", "2", "3" };
287     * data[2] = new String[]{ "A1", "B2", "C3" };
288     * </code>
289     * </pre>
290     *
291     * <p>
292     * Calling this method will print:
293     * </p>
294     *
295     * <pre>
296     * <code>
297     * A, B, C
298     * 1, 2, 3
299     * A1, B2, C3
300     * </code>
301     * </pre>
302     *
303     * @param values
304     *            the values to print.
305     * @throws IOException
306     *             If an I/O error occurs
307     */
308    public void printRecords(final Object... values) throws IOException {
309        for (final Object value : values) {
310            if (value instanceof Object[]) {
311                this.printRecord((Object[]) value);
312            } else if (value instanceof Iterable) {
313                this.printRecord((Iterable<?>) value);
314            } else {
315                this.printRecord(value);
316            }
317        }
318    }
319
320    /**
321     * Prints all the objects in the given JDBC result set.
322     *
323     * @param resultSet
324     *            result set the values to print.
325     * @throws IOException
326     *             If an I/O error occurs
327     * @throws SQLException
328     *             if a database access error occurs
329     */
330    public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
331        final int columnCount = resultSet.getMetaData().getColumnCount();
332        while (resultSet.next()) {
333            for (int i = 1; i <= columnCount; i++) {
334                print(resultSet.getObject(i));
335            }
336            println();
337        }
338    }
339}