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