View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.csv;
19  
20  import static org.apache.commons.csv.Constants.CR;
21  import static org.apache.commons.csv.Constants.LF;
22  import static org.apache.commons.csv.Constants.SP;
23  
24  import java.io.Closeable;
25  import java.io.Flushable;
26  import java.io.IOException;
27  import java.sql.Clob;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.util.Arrays;
31  import java.util.Objects;
32  import java.util.stream.Stream;
33  
34  /**
35   * Prints values in a {@link CSVFormat CSV format}.
36   *
37   * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
38   * Values are printed according to {@link String#valueOf(Object)}.
39   * To complete a record the {@link #println()} method has to be called.
40   * Comments can be appended by calling {@link #printComment(String)}.
41   * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
42   * </p>
43   *
44   * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
45   * or {@link #printRecord(Iterable)}.
46   * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
47   * methods can be used to print several records at once.
48   * </p>
49   *
50   * <p>Example:</p>
51   *
52   * <pre>
53   * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
54   *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
55   *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
56   *     printer.println();
57   *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
58   * } catch (IOException ex) {
59   *     ex.printStackTrace();
60   * }
61   * </pre>
62   *
63   * <p>This code will write the following to csv.txt:</p>
64   * <pre>
65   * id,userName,firstName,lastName,birthday
66   * 1,john73,John,Doe,1973-09-15
67   *
68   * 2,mary,Mary,Meyer,1985-03-29
69   * </pre>
70   */
71  public final class CSVPrinter implements Flushable, Closeable {
72  
73      /** The place that the values get written. */
74      private final Appendable appendable;
75  
76      private final CSVFormat format;
77  
78      /** True if we just began a new record. */
79      private boolean newRecord = true;
80  
81      /**
82       * Creates a printer that will print values to the given stream following the CSVFormat.
83       * <p>
84       * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
85       * and escaping with a different character) are not supported.
86       * </p>
87       *
88       * @param appendable
89       *            stream to which to print. Must not be null.
90       * @param format
91       *            the CSV format. Must not be null.
92       * @throws IOException
93       *             thrown if the optional header cannot be printed.
94       * @throws IllegalArgumentException
95       *             thrown if the parameters of the format are inconsistent or if either out or format are null.
96       */
97      public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException {
98          Objects.requireNonNull(appendable, "appendable");
99          Objects.requireNonNull(format, "format");
100 
101         this.appendable = appendable;
102         this.format = format.copy();
103         // TODO: Is it a good idea to do this here instead of on the first call to a print method?
104         // It seems a pain to have to track whether the header has already been printed or not.
105         final String[] headerComments = format.getHeaderComments();
106         if (headerComments != null) {
107             for (final String line : headerComments) {
108                 this.printComment(line);
109             }
110         }
111         if (format.getHeader() != null && !format.getSkipHeaderRecord()) {
112             this.printRecord((Object[]) format.getHeader());
113         }
114     }
115 
116     @Override
117     public void close() throws IOException {
118         close(false);
119     }
120 
121     /**
122      * Closes the underlying stream with an optional flush first.
123      * @param flush whether to flush before the actual close.
124      *
125      * @throws IOException
126      *             If an I/O error occurs
127      * @since 1.6
128      */
129     public void close(final boolean flush) throws IOException {
130         if (flush || format.getAutoFlush()) {
131             flush();
132         }
133         if (appendable instanceof Closeable) {
134             ((Closeable) appendable).close();
135         }
136     }
137 
138     /**
139      * Flushes the underlying stream.
140      *
141      * @throws IOException
142      *             If an I/O error occurs
143      */
144     @Override
145     public void flush() throws IOException {
146         if (appendable instanceof Flushable) {
147             ((Flushable) appendable).flush();
148         }
149     }
150 
151     /**
152      * Gets the target Appendable.
153      *
154      * @return the target Appendable.
155      */
156     public Appendable getOut() {
157         return this.appendable;
158     }
159 
160     /**
161      * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
162      *
163      * @param value
164      *            value to be output.
165      * @throws IOException
166      *             If an I/O error occurs
167      */
168     public synchronized void print(final Object value) throws IOException {
169         format.print(value, appendable, newRecord);
170         newRecord = false;
171     }
172 
173     /**
174      * Prints a comment on a new line among the delimiter separated values.
175      *
176      * <p>
177      * Comments will always begin on a new line and occupy at least one full line. The character specified to start
178      * comments and a space will be inserted at the beginning of each new line in the comment.
179      * </p>
180      *
181      * <p>
182      * If comments are disabled in the current CSV format this method does nothing.
183      * </p>
184      *
185      * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()}
186      * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use
187      * line breaks as record separator.</p>
188      *
189      * @param comment
190      *            the comment to output
191      * @throws IOException
192      *             If an I/O error occurs
193      */
194     public synchronized void printComment(final String comment) throws IOException {
195         if (comment == null || !format.isCommentMarkerSet()) {
196             return;
197         }
198         if (!newRecord) {
199             println();
200         }
201         appendable.append(format.getCommentMarker().charValue());
202         appendable.append(SP);
203         for (int i = 0; i < comment.length(); i++) {
204             final char c = comment.charAt(i);
205             switch (c) {
206             case CR:
207                 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
208                     i++;
209                 }
210                 //$FALL-THROUGH$ break intentionally excluded.
211             case LF:
212                 println();
213                 appendable.append(format.getCommentMarker().charValue());
214                 appendable.append(SP);
215                 break;
216             default:
217                 appendable.append(c);
218                 break;
219             }
220         }
221         println();
222     }
223 
224     /**
225      * Prints headers for a result set based on its metadata.
226      *
227      * @param resultSet The result set to query for metadata.
228      * @throws IOException If an I/O error occurs.
229      * @throws SQLException If a database access error occurs or this method is called on a closed result set.
230      * @since 1.9.0
231      */
232     public synchronized void printHeaders(final ResultSet resultSet) throws IOException, SQLException {
233         printRecord((Object[]) format.builder().setHeader(resultSet).build().getHeader());
234     }
235 
236     /**
237      * Outputs the record separator.
238      *
239      * @throws IOException
240      *             If an I/O error occurs
241      */
242     public synchronized void println() throws IOException {
243         format.println(appendable);
244         newRecord = true;
245     }
246 
247     /**
248      * Prints the given values as a single record of delimiter separated values followed by the record separator.
249      *
250      * <p>
251      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
252      * separator to the output after printing the record, so there is no need to call {@link #println()}.
253      * </p>
254      *
255      * @param values
256      *            values to output.
257      * @throws IOException
258      *             If an I/O error occurs
259      */
260     public synchronized void printRecord(final Iterable<?> values) throws IOException {
261         for (final Object value : values) {
262             print(value);
263         }
264         println();
265     }
266 
267     /**
268      * Prints the given values as a single record of delimiter separated values followed by the record separator.
269      *
270      * <p>
271      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
272      * separator to the output after printing the record, so there is no need to call {@link #println()}.
273      * </p>
274      *
275      * @param values
276      *            values to output.
277      * @throws IOException
278      *             If an I/O error occurs
279      */
280     public void printRecord(final Object... values) throws IOException {
281         printRecord(Arrays.asList(values));
282     }
283 
284     /**
285      * Prints the given values as a single record of delimiter separated values followed by the record separator.
286      *
287      * <p>
288      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
289      * separator to the output after printing the record, so there is no need to call {@link #println()}.
290      * </p>
291      *
292      * @param values
293      *            values to output.
294      * @throws IOException
295      *             If an I/O error occurs
296      * @since 1.10.0
297      */
298     public synchronized void printRecord(final Stream<?> values) throws IOException {
299         values.forEachOrdered(t -> {
300             try {
301                 print(t);
302             } catch (final IOException e) {
303                 throw IOUtils.rethrow(e);
304             }
305         });
306         println();
307     }
308 
309     private void printRecordObject(final Object value) throws IOException {
310         if (value instanceof Object[]) {
311             this.printRecord((Object[]) value);
312         } else if (value instanceof Iterable) {
313             this.printRecord((Iterable<?>) value);
314         } else {
315             this.printRecord(value);
316         }
317     }
318 
319     /**
320      * Prints all the objects in the given {@link Iterable} handling nested collections/arrays as records.
321      *
322      * <p>
323      * If the given Iterable only contains simple objects, this method will print a single record like
324      * {@link #printRecord(Iterable)}. If the given Iterable contains nested collections/arrays those nested elements
325      * will each be printed as records using {@link #printRecord(Object...)}.
326      * </p>
327      *
328      * <p>
329      * Given the following data structure:
330      * </p>
331      *
332      * <pre>
333      * <code>
334      * List&lt;String[]&gt; data = new ArrayList&lt;&gt;();
335      * data.add(new String[]{ "A", "B", "C" });
336      * data.add(new String[]{ "1", "2", "3" });
337      * data.add(new String[]{ "A1", "B2", "C3" });
338      * </code>
339      * </pre>
340      *
341      * <p>
342      * Calling this method will print:
343      * </p>
344      *
345      * <pre>
346      * <code>
347      * A, B, C
348      * 1, 2, 3
349      * A1, B2, C3
350      * </code>
351      * </pre>
352      *
353      * @param values
354      *            the values to print.
355      * @throws IOException
356      *             If an I/O error occurs
357      */
358     public void printRecords(final Iterable<?> values) throws IOException {
359         for (final Object value : values) {
360             printRecordObject(value);
361         }
362     }
363 
364     /**
365      * Prints all the objects in the given array handling nested collections/arrays as records.
366      *
367      * <p>
368      * If the given array only contains simple objects, this method will print a single record like
369      * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
370      * elements will each be printed as records using {@link #printRecord(Object...)}.
371      * </p>
372      *
373      * <p>
374      * Given the following data structure:
375      * </p>
376      *
377      * <pre>
378      * <code>
379      * String[][] data = new String[3][]
380      * data[0] = String[]{ "A", "B", "C" };
381      * data[1] = new String[]{ "1", "2", "3" };
382      * data[2] = new String[]{ "A1", "B2", "C3" };
383      * </code>
384      * </pre>
385      *
386      * <p>
387      * Calling this method will print:
388      * </p>
389      *
390      * <pre>
391      * <code>
392      * A, B, C
393      * 1, 2, 3
394      * A1, B2, C3
395      * </code>
396      * </pre>
397      *
398      * @param values
399      *            the values to print.
400      * @throws IOException
401      *             If an I/O error occurs
402      */
403     public void printRecords(final Object... values) throws IOException {
404         printRecords(Arrays.asList(values));
405     }
406 
407     /**
408      * Prints all the objects in the given JDBC result set.
409      *
410      * @param resultSet
411      *            result set the values to print.
412      * @throws IOException
413      *             If an I/O error occurs
414      * @throws SQLException
415      *             if a database access error occurs
416      */
417     public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
418         final int columnCount = resultSet.getMetaData().getColumnCount();
419         while (resultSet.next()) {
420             for (int i = 1; i <= columnCount; i++) {
421                 final Object object = resultSet.getObject(i);
422                 // TODO Who manages the Clob? The JDBC driver or must we close it? Is it driver-dependent?
423                 print(object instanceof Clob ? ((Clob) object).getCharacterStream() : object);
424             }
425             println();
426         }
427     }
428 
429     /**
430      * Prints all the objects with metadata in the given JDBC result set based on the header boolean.
431      *
432      * @param resultSet source of row data.
433      * @param printHeader whether to print headers.
434      * @throws IOException If an I/O error occurs
435      * @throws SQLException if a database access error occurs
436      * @since 1.9.0
437      */
438     public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException {
439         if (printHeader) {
440             printHeaders(resultSet);
441         }
442         printRecords(resultSet);
443     }
444 
445     /**
446      * Prints all the objects in the given {@link Stream} handling nested collections/arrays as records.
447      *
448      * <p>
449      * If the given Stream only contains simple objects, this method will print a single record like
450      * {@link #printRecord(Iterable)}. If the given Stream contains nested collections/arrays those nested elements
451      * will each be printed as records using {@link #printRecord(Object...)}.
452      * </p>
453      *
454      * <p>
455      * Given the following data structure:
456      * </p>
457      *
458      * <pre>
459      * <code>
460      * List&lt;String[]&gt; data = new ArrayList&lt;&gt;();
461      * data.add(new String[]{ "A", "B", "C" });
462      * data.add(new String[]{ "1", "2", "3" });
463      * data.add(new String[]{ "A1", "B2", "C3" });
464      * Stream&lt;String[]&gt; stream = data.stream();
465      * </code>
466      * </pre>
467      *
468      * <p>
469      * Calling this method will print:
470      * </p>
471      *
472      * <pre>
473      * <code>
474      * A, B, C
475      * 1, 2, 3
476      * A1, B2, C3
477      * </code>
478      * </pre>
479      *
480      * @param values
481      *            the values to print.
482      * @throws IOException
483      *             If an I/O error occurs
484      * @since 1.10.0
485      */
486     @SuppressWarnings("unused") // rethrow() throws IOException
487     public void printRecords(final Stream<?> values) throws IOException {
488         values.forEachOrdered(t -> {
489             try {
490                 printRecordObject(t);
491             } catch (final IOException e) {
492                 throw IOUtils.rethrow(e);
493             }
494         });
495     }
496 }