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.Clob;
028import java.sql.ResultSet;
029import java.sql.SQLException;
030import java.util.Arrays;
031import java.util.Objects;
032import java.util.stream.Stream;
033
034/**
035 * Prints values in a {@link CSVFormat CSV format}.
036 *
037 * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
038 * Values are printed according to {@link String#valueOf(Object)}.
039 * To complete a record the {@link #println()} method has to be called.
040 * Comments can be appended by calling {@link #printComment(String)}.
041 * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
042 * </p>
043 *
044 * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
045 * or {@link #printRecord(Iterable)}.
046 * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
047 * methods can be used to print several records at once.
048 * </p>
049 *
050 * <p>Example:</p>
051 *
052 * <pre>
053 * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
054 *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
055 *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
056 *     printer.println();
057 *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
058 * } catch (IOException ex) {
059 *     ex.printStackTrace();
060 * }
061 * </pre>
062 *
063 * <p>This code will write the following to csv.txt:</p>
064 * <pre>
065 * id,userName,firstName,lastName,birthday
066 * 1,john73,John,Doe,1973-09-15
067 *
068 * 2,mary,Mary,Meyer,1985-03-29
069 * </pre>
070 */
071public final class CSVPrinter implements Flushable, Closeable {
072
073    /** The place that the values get written. */
074    private final Appendable appendable;
075
076    private final CSVFormat format;
077
078    /** True if we just began a new record. */
079    private boolean newRecord = true;
080
081    /**
082     * Creates a printer that will print values to the given stream following the CSVFormat.
083     * <p>
084     * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
085     * and escaping with a different character) are not supported.
086     * </p>
087     *
088     * @param appendable
089     *            stream to which to print. Must not be null.
090     * @param format
091     *            the CSV format. Must not be null.
092     * @throws IOException
093     *             thrown if the optional header cannot be printed.
094     * @throws IllegalArgumentException
095     *             thrown if the parameters of the format are inconsistent or if either out or format are null.
096     */
097    public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException {
098        Objects.requireNonNull(appendable, "appendable");
099        Objects.requireNonNull(format, "format");
100
101        this.appendable = appendable;
102        this.format = format.copy();
103        // TODO: Is it a good idea to do this here instead of on the first call to a print method?
104        // It seems a pain to have to track whether the header has already been printed or not.
105        final String[] headerComments = format.getHeaderComments();
106        if (headerComments != null) {
107            for (final String line : headerComments) {
108                this.printComment(line);
109            }
110        }
111        if (format.getHeader() != null && !format.getSkipHeaderRecord()) {
112            this.printRecord((Object[]) format.getHeader());
113        }
114    }
115
116    @Override
117    public void close() throws IOException {
118        close(false);
119    }
120
121    /**
122     * Closes the underlying stream with an optional flush first.
123     * @param flush whether to flush before the actual close.
124     *
125     * @throws IOException
126     *             If an I/O error occurs
127     * @since 1.6
128     */
129    public void close(final boolean flush) throws IOException {
130        if (flush || format.getAutoFlush()) {
131            flush();
132        }
133        if (appendable instanceof Closeable) {
134            ((Closeable) appendable).close();
135        }
136    }
137
138    /**
139     * Flushes the underlying stream.
140     *
141     * @throws IOException
142     *             If an I/O error occurs
143     */
144    @Override
145    public void flush() throws IOException {
146        if (appendable instanceof Flushable) {
147            ((Flushable) appendable).flush();
148        }
149    }
150
151    /**
152     * Gets the target Appendable.
153     *
154     * @return the target Appendable.
155     */
156    public Appendable getOut() {
157        return this.appendable;
158    }
159
160    /**
161     * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
162     *
163     * @param value
164     *            value to be output.
165     * @throws IOException
166     *             If an I/O error occurs
167     */
168    public synchronized void print(final Object value) throws IOException {
169        format.print(value, appendable, newRecord);
170        newRecord = false;
171    }
172
173    /**
174     * Prints a comment on a new line among the delimiter separated values.
175     *
176     * <p>
177     * Comments will always begin on a new line and occupy at least one full line. The character specified to start
178     * comments and a space will be inserted at the beginning of each new line in the comment.
179     * </p>
180     *
181     * <p>
182     * If comments are disabled in the current CSV format this method does nothing.
183     * </p>
184     *
185     * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()}
186     * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use
187     * line breaks as record separator.</p>
188     *
189     * @param comment
190     *            the comment to output
191     * @throws IOException
192     *             If an I/O error occurs
193     */
194    public synchronized void printComment(final String comment) throws IOException {
195        if (comment == null || !format.isCommentMarkerSet()) {
196            return;
197        }
198        if (!newRecord) {
199            println();
200        }
201        appendable.append(format.getCommentMarker().charValue());
202        appendable.append(SP);
203        for (int i = 0; i < comment.length(); i++) {
204            final char c = comment.charAt(i);
205            switch (c) {
206            case CR:
207                if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
208                    i++;
209                }
210                //$FALL-THROUGH$ break intentionally excluded.
211            case LF:
212                println();
213                appendable.append(format.getCommentMarker().charValue());
214                appendable.append(SP);
215                break;
216            default:
217                appendable.append(c);
218                break;
219            }
220        }
221        println();
222    }
223
224    /**
225     * Prints headers for a result set based on its metadata.
226     *
227     * @param resultSet The result set to query for metadata.
228     * @throws IOException If an I/O error occurs.
229     * @throws SQLException If a database access error occurs or this method is called on a closed result set.
230     * @since 1.9.0
231     */
232    public synchronized void printHeaders(final ResultSet resultSet) throws IOException, SQLException {
233        printRecord((Object[]) format.builder().setHeader(resultSet).build().getHeader());
234    }
235
236    /**
237     * Outputs the record separator.
238     *
239     * @throws IOException
240     *             If an I/O error occurs
241     */
242    public synchronized void println() throws IOException {
243        format.println(appendable);
244        newRecord = true;
245    }
246
247    /**
248     * Prints the given values as a single record of delimiter separated values followed by the record separator.
249     *
250     * <p>
251     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
252     * separator to the output after printing the record, so there is no need to call {@link #println()}.
253     * </p>
254     *
255     * @param values
256     *            values to output.
257     * @throws IOException
258     *             If an I/O error occurs
259     */
260    public synchronized void printRecord(final Iterable<?> values) throws IOException {
261        for (final Object value : values) {
262            print(value);
263        }
264        println();
265    }
266
267    /**
268     * Prints the given values as a single record of delimiter separated values followed by the record separator.
269     *
270     * <p>
271     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
272     * separator to the output after printing the record, so there is no need to call {@link #println()}.
273     * </p>
274     *
275     * @param values
276     *            values to output.
277     * @throws IOException
278     *             If an I/O error occurs
279     */
280    public void printRecord(final Object... values) throws IOException {
281        printRecord(Arrays.asList(values));
282    }
283
284    /**
285     * Prints the given values as a single record of delimiter separated values followed by the record separator.
286     *
287     * <p>
288     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
289     * separator to the output after printing the record, so there is no need to call {@link #println()}.
290     * </p>
291     *
292     * @param values
293     *            values to output.
294     * @throws IOException
295     *             If an I/O error occurs
296     * @since 1.10.0
297     */
298    public synchronized void printRecord(final Stream<?> values) throws IOException {
299        values.forEachOrdered(t -> {
300            try {
301                print(t);
302            } catch (final IOException e) {
303                throw IOUtils.rethrow(e);
304            }
305        });
306        println();
307    }
308
309    private void printRecordObject(final Object value) throws IOException {
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     * Prints all the objects in the given {@link Iterable} handling nested collections/arrays as records.
321     *
322     * <p>
323     * If the given Iterable only contains simple objects, this method will print a single record like
324     * {@link #printRecord(Iterable)}. If the given Iterable contains nested collections/arrays those nested elements
325     * will each be printed as records using {@link #printRecord(Object...)}.
326     * </p>
327     *
328     * <p>
329     * Given the following data structure:
330     * </p>
331     *
332     * <pre>
333     * <code>
334     * List&lt;String[]&gt; data = new ArrayList&lt;&gt;();
335     * data.add(new String[]{ "A", "B", "C" });
336     * data.add(new String[]{ "1", "2", "3" });
337     * data.add(new String[]{ "A1", "B2", "C3" });
338     * </code>
339     * </pre>
340     *
341     * <p>
342     * Calling this method will print:
343     * </p>
344     *
345     * <pre>
346     * <code>
347     * A, B, C
348     * 1, 2, 3
349     * A1, B2, C3
350     * </code>
351     * </pre>
352     *
353     * @param values
354     *            the values to print.
355     * @throws IOException
356     *             If an I/O error occurs
357     */
358    public void printRecords(final Iterable<?> values) throws IOException {
359        for (final Object value : values) {
360            printRecordObject(value);
361        }
362    }
363
364    /**
365     * Prints all the objects in the given array handling nested collections/arrays as records.
366     *
367     * <p>
368     * If the given array only contains simple objects, this method will print a single record like
369     * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
370     * elements will each be printed as records using {@link #printRecord(Object...)}.
371     * </p>
372     *
373     * <p>
374     * Given the following data structure:
375     * </p>
376     *
377     * <pre>
378     * <code>
379     * String[][] data = new String[3][]
380     * data[0] = String[]{ "A", "B", "C" };
381     * data[1] = new String[]{ "1", "2", "3" };
382     * data[2] = new String[]{ "A1", "B2", "C3" };
383     * </code>
384     * </pre>
385     *
386     * <p>
387     * Calling this method will print:
388     * </p>
389     *
390     * <pre>
391     * <code>
392     * A, B, C
393     * 1, 2, 3
394     * A1, B2, C3
395     * </code>
396     * </pre>
397     *
398     * @param values
399     *            the values to print.
400     * @throws IOException
401     *             If an I/O error occurs
402     */
403    public void printRecords(final Object... values) throws IOException {
404        printRecords(Arrays.asList(values));
405    }
406
407    /**
408     * Prints all the objects in the given JDBC result set.
409     *
410     * @param resultSet
411     *            result set the values to print.
412     * @throws IOException
413     *             If an I/O error occurs
414     * @throws SQLException
415     *             if a database access error occurs
416     */
417    public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
418        final int columnCount = resultSet.getMetaData().getColumnCount();
419        while (resultSet.next()) {
420            for (int i = 1; i <= columnCount; i++) {
421                final Object object = resultSet.getObject(i);
422                // TODO Who manages the Clob? The JDBC driver or must we close it? Is it driver-dependent?
423                print(object instanceof Clob ? ((Clob) object).getCharacterStream() : object);
424            }
425            println();
426        }
427    }
428
429    /**
430     * Prints all the objects with metadata in the given JDBC result set based on the header boolean.
431     *
432     * @param resultSet source of row data.
433     * @param printHeader whether to print headers.
434     * @throws IOException If an I/O error occurs
435     * @throws SQLException if a database access error occurs
436     * @since 1.9.0
437     */
438    public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException {
439        if (printHeader) {
440            printHeaders(resultSet);
441        }
442        printRecords(resultSet);
443    }
444
445    /**
446     * Prints all the objects in the given {@link Stream} handling nested collections/arrays as records.
447     *
448     * <p>
449     * If the given Stream only contains simple objects, this method will print a single record like
450     * {@link #printRecord(Iterable)}. If the given Stream contains nested collections/arrays those nested elements
451     * will each be printed as records using {@link #printRecord(Object...)}.
452     * </p>
453     *
454     * <p>
455     * Given the following data structure:
456     * </p>
457     *
458     * <pre>
459     * <code>
460     * List&lt;String[]&gt; data = new ArrayList&lt;&gt;();
461     * data.add(new String[]{ "A", "B", "C" });
462     * data.add(new String[]{ "1", "2", "3" });
463     * data.add(new String[]{ "A1", "B2", "C3" });
464     * Stream&lt;String[]&gt; stream = data.stream();
465     * </code>
466     * </pre>
467     *
468     * <p>
469     * Calling this method will print:
470     * </p>
471     *
472     * <pre>
473     * <code>
474     * A, B, C
475     * 1, 2, 3
476     * A1, B2, C3
477     * </code>
478     * </pre>
479     *
480     * @param values
481     *            the values to print.
482     * @throws IOException
483     *             If an I/O error occurs
484     * @since 1.10.0
485     */
486    @SuppressWarnings("unused") // rethrow() throws IOException
487    public void printRecords(final Stream<?> values) throws IOException {
488        values.forEachOrdered(t -> {
489            try {
490                printRecordObject(t);
491            } catch (final IOException e) {
492                throw IOUtils.rethrow(e);
493            }
494        });
495    }
496}