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