View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.commons.csv;
21  
22  import static org.apache.commons.csv.Constants.CR;
23  import static org.apache.commons.csv.Constants.LF;
24  import static org.apache.commons.csv.Constants.SP;
25  
26  import java.io.Closeable;
27  import java.io.Flushable;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.Reader;
31  import java.sql.Blob;
32  import java.sql.Clob;
33  import java.sql.ResultSet;
34  import java.sql.SQLException;
35  import java.sql.Statement;
36  import java.util.Arrays;
37  import java.util.Objects;
38  import java.util.concurrent.locks.ReentrantLock;
39  import java.util.stream.Stream;
40  
41  import org.apache.commons.io.function.IOStream;
42  
43  /**
44   * Prints values in a {@link CSVFormat CSV format}.
45   *
46   * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
47   * Values are printed according to {@link String#valueOf(Object)}.
48   * To complete a record the {@link #println()} method has to be called.
49   * Comments can be appended by calling {@link #printComment(String)}.
50   * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
51   * </p>
52   *
53   * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
54   * or {@link #printRecord(Iterable)}.
55   * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
56   * methods can be used to print several records at once.
57   * </p>
58   *
59   * <p>Example:</p>
60   *
61   * <pre>
62   * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
63   *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
64   *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
65   *     printer.println();
66   *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
67   * } catch (IOException ex) {
68   *     ex.printStackTrace();
69   * }
70   * </pre>
71   *
72   * <p>This code will write the following to csv.txt:</p>
73   * <pre>
74   * id,userName,firstName,lastName,birthday
75   * 1,john73,John,Doe,1973-09-15
76   *
77   * 2,mary,Mary,Meyer,1985-03-29
78   * </pre>
79   */
80  public final class CSVPrinter implements Flushable, Closeable {
81  
82      /** The place that the values get written. */
83      private final Appendable appendable;
84  
85      private final CSVFormat format;
86  
87      /** True if we just began a new record. */
88      private boolean newRecord = true;
89  
90      private long recordCount;
91  
92      private final ReentrantLock lock = new ReentrantLock();
93  
94      /**
95       * Creates a printer that will print values to the given stream following the CSVFormat.
96       * <p>
97       * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation and escaping with a different
98       * character) are not supported.
99       * </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 }