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