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