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