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