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.util.Arrays;
36  import java.util.Objects;
37  import java.util.stream.Stream;
38  
39  import org.apache.commons.io.function.IOStream;
40  
41  /**
42   * Prints values in a {@link CSVFormat CSV format}.
43   *
44   * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
45   * Values are printed according to {@link String#valueOf(Object)}.
46   * To complete a record the {@link #println()} method has to be called.
47   * Comments can be appended by calling {@link #printComment(String)}.
48   * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
49   * </p>
50   *
51   * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
52   * or {@link #printRecord(Iterable)}.
53   * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
54   * methods can be used to print several records at once.
55   * </p>
56   *
57   * <p>Example:</p>
58   *
59   * <pre>
60   * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
61   *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
62   *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
63   *     printer.println();
64   *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
65   * } catch (IOException ex) {
66   *     ex.printStackTrace();
67   * }
68   * </pre>
69   *
70   * <p>This code will write the following to csv.txt:</p>
71   * <pre>
72   * id,userName,firstName,lastName,birthday
73   * 1,john73,John,Doe,1973-09-15
74   *
75   * 2,mary,Mary,Meyer,1985-03-29
76   * </pre>
77   */
78  public final class CSVPrinter implements Flushable, Closeable {
79  
80      /** The place that the values get written. */
81      private final Appendable appendable;
82  
83      private final CSVFormat format;
84  
85      /** True if we just began a new record. */
86      private boolean newRecord = true;
87  
88      private long recordCount;
89  
90      /**
91       * Creates a printer that will print values to the given stream following the CSVFormat.
92       * <p>
93       * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
94       * and escaping with a different character) are not supported.
95       * </p>
96       *
97       * @param appendable
98       *            stream to which to print. Must not be null.
99       * @param format
100      *            the CSV format. Must not be null.
101      * @throws IOException
102      *             thrown if the optional header cannot be printed.
103      * @throws IllegalArgumentException
104      *             thrown if the parameters of the format are inconsistent or if either out or format are null.
105      */
106     public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException {
107         Objects.requireNonNull(appendable, "appendable");
108         Objects.requireNonNull(format, "format");
109 
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      * @param flush whether to flush before the actual close.
133      * @throws IOException
134      *             If an I/O error occurs
135      * @since 1.6
136      */
137     public void close(final boolean flush) throws IOException {
138         if (flush || format.getAutoFlush()) {
139             flush();
140         }
141         if (appendable instanceof Closeable) {
142             ((Closeable) appendable).close();
143         }
144     }
145 
146     /**
147      * Outputs the record separator and increments the record count.
148      *
149      * @throws IOException
150      *             If an I/O error occurs
151      */
152     private synchronized void endOfRecord() throws IOException {
153         println();
154         recordCount++;
155     }
156 
157     /**
158      * Flushes the underlying stream.
159      *
160      * @throws IOException
161      *             If an I/O error occurs
162      */
163     @Override
164     public void flush() throws IOException {
165         if (appendable instanceof Flushable) {
166             ((Flushable) appendable).flush();
167         }
168     }
169 
170     /**
171      * Gets the target Appendable.
172      *
173      * @return the target Appendable.
174      */
175     public Appendable getOut() {
176         return this.appendable;
177     }
178 
179     /**
180      * Gets the record count printed, this does not include comments or headers.
181      *
182      * @return the record count, this does not include comments or headers.
183      * @since 1.13.0
184      */
185     public long getRecordCount() {
186         return recordCount;
187     }
188 
189     /**
190      * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
191      *
192      * @param value
193      *            value to be output.
194      * @throws IOException
195      *             If an I/O error occurs
196      */
197     public synchronized void print(final Object value) throws IOException {
198         format.print(value, appendable, newRecord);
199         newRecord = false;
200     }
201 
202     /**
203      * Prints a comment on a new line among the delimiter-separated values.
204      *
205      * <p>
206      * Comments will always begin on a new line and occupy at least one full line. The character specified to start
207      * comments and a space will be inserted at the beginning of each new line in the comment.
208      * </p>
209      *
210      * <p>
211      * If comments are disabled in the current CSV format this method does nothing.
212      * </p>
213      *
214      * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()}
215      * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use
216      * line breaks as record separators.</p>
217      *
218      * @param comment
219      *            the comment to output
220      * @throws IOException
221      *             If an I/O error occurs
222      */
223     public synchronized void printComment(final String comment) throws IOException {
224         if (comment == null || !format.isCommentMarkerSet()) {
225             return;
226         }
227         if (!newRecord) {
228             println();
229         }
230         appendable.append(format.getCommentMarker().charValue()); // N.B. Explicit (un)boxing is intentional
231         appendable.append(SP);
232         for (int i = 0; i < comment.length(); i++) {
233             final char c = comment.charAt(i);
234             switch (c) {
235             case CR:
236                 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
237                     i++;
238                 }
239                 // falls-through: break intentionally excluded.
240             case LF:
241                 println();
242                 appendable.append(format.getCommentMarker().charValue()); // N.B. Explicit (un)boxing is intentional
243                 appendable.append(SP);
244                 break;
245             default:
246                 appendable.append(c);
247                 break;
248             }
249         }
250         println();
251     }
252 
253     /**
254      * Prints headers for a result set based on its metadata.
255      *
256      * @param resultSet The ResultSet to query for metadata.
257      * @throws IOException If an I/O error occurs.
258      * @throws SQLException If a database access error occurs or this method is called on a closed result set.
259      * @since 1.9.0
260      */
261     public synchronized void printHeaders(final ResultSet resultSet) throws IOException, SQLException {
262         try (IOStream<String> stream = IOStream.of(format.builder().setHeader(resultSet).get().getHeader())) {
263             stream.forEachOrdered(this::print);
264         }
265         println();
266     }
267 
268     /**
269      * Outputs the record separator.
270      *
271      * @throws IOException
272      *             If an I/O error occurs
273      */
274     public synchronized void println() throws IOException {
275         format.println(appendable);
276         newRecord = true;
277     }
278 
279     /**
280      * Prints the given values as a single record of delimiter-separated values followed by the record separator.
281      *
282      * <p>
283      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
284      * separator to the output after printing the record, so there is no need to call {@link #println()}.
285      * </p>
286      *
287      * @param values
288      *            values to output.
289      * @throws IOException
290      *             If an I/O error occurs
291      */
292     @SuppressWarnings("resource")
293     public synchronized void printRecord(final Iterable<?> values) throws IOException {
294         IOStream.of(values).forEachOrdered(this::print);
295         endOfRecord();
296     }
297 
298     /**
299      * Prints the given values as a single record of delimiter-separated values followed by the record separator.
300      *
301      * <p>
302      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
303      * separator to the output after printing the record, so there is no need to call {@link #println()}.
304      * </p>
305      *
306      * @param values
307      *            values to output.
308      * @throws IOException
309      *             If an I/O error occurs
310      */
311     public void printRecord(final Object... values) throws IOException {
312         printRecord(Arrays.asList(values));
313     }
314 
315     /**
316      * Prints the given values as a single record of delimiter-separated values followed by the record separator.
317      *
318      * <p>
319      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
320      * separator to the output after printing the record, so there is no need to call {@link #println()}.
321      * </p>
322      *
323      * @param values
324      *            values to output.
325      * @throws IOException
326      *             If an I/O error occurs
327      * @since 1.10.0
328      */
329     @SuppressWarnings("resource") // caller closes.
330     public synchronized void printRecord(final Stream<?> values) throws IOException {
331         IOStream.adapt(values).forEachOrdered(this::print);
332         endOfRecord();
333     }
334 
335     private void printRecordObject(final Object value) throws IOException {
336         if (value instanceof Object[]) {
337             this.printRecord((Object[]) value);
338         } else if (value instanceof Iterable) {
339             this.printRecord((Iterable<?>) value);
340         } else {
341             this.printRecord(value);
342         }
343     }
344 
345     /**
346      * Prints all the objects in the given {@link Iterable} handling nested collections/arrays as records.
347      *
348      * <p>
349      * If the given Iterable only contains simple objects, this method will print a single record like
350      * {@link #printRecord(Iterable)}. If the given Iterable contains nested collections/arrays those nested elements
351      * will each be printed as records using {@link #printRecord(Object...)}.
352      * </p>
353      *
354      * <p>
355      * Given the following data structure:
356      * </p>
357      *
358      * <pre>{@code
359      * List<String[]> data = new ArrayList<>();
360      * data.add(new String[]{ "A", "B", "C" });
361      * data.add(new String[]{ "1", "2", "3" });
362      * data.add(new String[]{ "A1", "B2", "C3" });
363      * }
364      * </pre>
365      *
366      * <p>
367      * Calling this method will print:
368      * </p>
369      *
370      * <pre>
371      * {@code
372      * A, B, C
373      * 1, 2, 3
374      * A1, B2, C3
375      * }
376      * </pre>
377      *
378      * @param values
379      *            the values to print.
380      * @throws IOException
381      *             If an I/O error occurs
382      */
383     @SuppressWarnings("resource")
384     public void printRecords(final Iterable<?> values) throws IOException {
385         IOStream.of(values).forEachOrdered(this::printRecordObject);
386     }
387 
388     /**
389      * Prints all the objects in the given array handling nested collections/arrays as records.
390      *
391      * <p>
392      * If the given array only contains simple objects, this method will print a single record like
393      * {@link #printRecord(Object...)}. If the given collections contain nested collections or arrays, those nested
394      * elements will each be printed as records using {@link #printRecord(Object...)}.
395      * </p>
396      *
397      * <p>
398      * Given the following data structure:
399      * </p>
400      *
401      * <pre>{@code
402      * String[][] data = new String[3][]
403      * data[0] = String[]{ "A", "B", "C" };
404      * data[1] = new String[]{ "1", "2", "3" };
405      * data[2] = new String[]{ "A1", "B2", "C3" };
406      * }
407      * </pre>
408      *
409      * <p>
410      * Calling this method will print:
411      * </p>
412      *
413      * <pre>{@code
414      * A, B, C
415      * 1, 2, 3
416      * A1, B2, C3
417      * }
418      * </pre>
419      *
420      * @param values
421      *            the values to print.
422      * @throws IOException
423      *             If an I/O error occurs
424      */
425     public void printRecords(final Object... values) throws IOException {
426         printRecords(Arrays.asList(values));
427     }
428 
429     /**
430      * Prints all the objects in the given JDBC result set.
431      *
432      * @param resultSet
433      *             The values to print.
434      * @throws IOException
435      *             If an I/O error occurs.
436      * @throws SQLException
437      *             Thrown when a database access error occurs.
438      */
439     public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
440         final int columnCount = resultSet.getMetaData().getColumnCount();
441         while (resultSet.next()) {
442             for (int i = 1; i <= columnCount; i++) {
443                 final Object object = resultSet.getObject(i);
444                 if (object instanceof Clob) {
445                     try (Reader reader = ((Clob) object).getCharacterStream()) {
446                         print(reader);
447                     }
448                 } else if (object instanceof Blob) {
449                     try (InputStream inputStream = ((Blob) object).getBinaryStream()) {
450                         print(inputStream);
451                     }
452                 } else {
453                     print(object);
454                 }
455             }
456             endOfRecord();
457         }
458     }
459 
460     /**
461      * Prints all the objects with metadata in the given JDBC result set based on the header boolean.
462      *
463      * @param resultSet source of row data.
464      * @param printHeader whether to print headers.
465      * @throws IOException If an I/O error occurs
466      * @throws SQLException if a database access error occurs
467      * @since 1.9.0
468      */
469     public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException {
470         if (printHeader) {
471             printHeaders(resultSet);
472         }
473         printRecords(resultSet);
474     }
475 
476     /**
477      * Prints all the objects in the given {@link Stream} handling nested collections/arrays as records.
478      *
479      * <p>
480      * If the given Stream only contains simple objects, this method will print a single record like
481      * {@link #printRecord(Iterable)}. If the given Stream contains nested collections/arrays those nested elements
482      * will each be printed as records using {@link #printRecord(Object...)}.
483      * </p>
484      *
485      * <p>
486      * Given the following data structure:
487      * </p>
488      *
489      * <pre>{@code
490      * List<String[]> data = new ArrayList<>();
491      * data.add(new String[]{ "A", "B", "C" });
492      * data.add(new String[]{ "1", "2", "3" });
493      * data.add(new String[]{ "A1", "B2", "C3" });
494      * Stream<String[]> stream = data.stream();
495      * }
496      * </pre>
497      *
498      * <p>
499      * Calling this method will print:
500      * </p>
501      *
502      * <pre>
503      * {@code
504      * A, B, C
505      * 1, 2, 3
506      * A1, B2, C3
507      * }
508      * </pre>
509      *
510      * @param values
511      *            the values to print.
512      * @throws IOException
513      *             If an I/O error occurs
514      * @since 1.10.0
515      */
516     @SuppressWarnings({ "resource" }) // Caller closes.
517     public void printRecords(final Stream<?> values) throws IOException {
518         IOStream.adapt(values).forEachOrdered(this::printRecordObject);
519     }
520 }