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