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  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44  
45  /**
46   * Prints values in a {@link CSVFormat CSV format}.
47   *
48   * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
49   * Values are printed according to {@link String#valueOf(Object)}.
50   * To complete a record the {@link #println()} method has to be called.
51   * Comments can be appended by calling {@link #printComment(String)}.
52   * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
53   * </p>
54   *
55   * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
56   * or {@link #printRecord(Iterable)}.
57   * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
58   * methods can be used to print several records at once.
59   * </p>
60   *
61   * <p>Example:</p>
62   *
63   * <pre>
64   * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
65   *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
66   *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
67   *     printer.println();
68   *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
69   * } catch (IOException ex) {
70   *     ex.printStackTrace();
71   * }
72   * </pre>
73   *
74   * <p>This code will write the following to csv.txt:</p>
75   * <pre>
76   * id,userName,firstName,lastName,birthday
77   * 1,john73,John,Doe,1973-09-15
78   *
79   * 2,mary,Mary,Meyer,1985-03-29
80   * </pre>
81   */
82  public final class CSVPrinter implements Flushable, Closeable {
83  
84      /** The place that the values get written. */
85      private final Appendable appendable;
86  
87      private final CSVFormat format;
88  
89      /** True if we just began a new record. */
90      private boolean newRecord = true;
91  
92      private long recordCount;
93  
94      private final ReentrantLock lock = new ReentrantLock();
95  
96      /**
97       * Creates a printer that will print values to the given stream following the CSVFormat.
98       * <p>
99       * 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 }