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.COMMENT;
21  import static org.apache.commons.csv.Constants.CR;
22  import static org.apache.commons.csv.Constants.LF;
23  import static org.apache.commons.csv.Constants.SP;
24  
25  import java.io.Closeable;
26  import java.io.Flushable;
27  import java.io.IOException;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  
31  /**
32   * Prints values in a CSV format.
33   *
34   * @version $Id: CSVPrinter.java 1617076 2014-08-10 09:23:01Z britter $
35   */
36  public final class CSVPrinter implements Flushable, Closeable {
37  
38      /** The place that the values get written. */
39      private final Appendable out;
40      private final CSVFormat format;
41  
42      /** True if we just began a new record. */
43      private boolean newRecord = true;
44  
45      /**
46       * Creates a printer that will print values to the given stream following the CSVFormat.
47       * <p>
48       * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
49       * and escaping with a different character) are not supported.
50       * </p>
51       *
52       * @param out
53       *        stream to which to print. Must not be null.
54       * @param format
55       *        the CSV format. Must not be null.
56       * @throws IOException
57       *         thrown if the optional header cannot be printed.
58       * @throws IllegalArgumentException
59       *         thrown if the parameters of the format are inconsistent or if either out or format are null.
60       */
61      public CSVPrinter(final Appendable out, final CSVFormat format) throws IOException {
62          Assertions.notNull(out, "out");
63          Assertions.notNull(format, "format");
64  
65          this.out = out;
66          this.format = format;
67          // TODO: Is it a good idea to do this here instead of on the first call to a print method?
68          // It seems a pain to have to track whether the header has already been printed or not.
69          if (format.getHeader() != null) {
70              this.printRecord((Object[]) format.getHeader());
71          }
72      }
73  
74      // ======================================================
75      // printing implementation
76      // ======================================================
77  
78      public void close() throws IOException {
79          if (out instanceof Closeable) {
80              ((Closeable) out).close();
81          }
82      }
83  
84      /**
85       * Flushes the underlying stream.
86       *
87       * @throws IOException
88       *             If an I/O error occurs
89       */
90      public void flush() throws IOException {
91          if (out instanceof Flushable) {
92              ((Flushable) out).flush();
93          }
94      }
95  
96      /**
97       * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
98       *
99       * @param value
100      *            value to be output.
101      * @throws IOException
102      *             If an I/O error occurs
103      */
104     public void print(final Object value) throws IOException {
105         // null values are considered empty
106         String strValue;
107         if (value == null) {
108             final String nullString = format.getNullString();
109             strValue = nullString == null ? Constants.EMPTY : nullString;
110         } else {
111             strValue = value.toString();
112         }
113         this.print(value, strValue, 0, strValue.length());
114     }
115 
116     private void print(final Object object, final CharSequence value,
117             final int offset, final int len) throws IOException {
118         if (!newRecord) {
119             out.append(format.getDelimiter());
120         }
121         if (format.isQuoteCharacterSet()) {
122             // the original object is needed so can check for Number
123             printAndQuote(object, value, offset, len);
124         } else if (format.isEscapeCharacterSet()) {
125             printAndEscape(value, offset, len);
126         } else {
127             out.append(value, offset, offset + len);
128         }
129         newRecord = false;
130     }
131 
132     /*
133      * Note: must only be called if escaping is enabled, otherwise will generate NPE
134      */
135     private void printAndEscape(final CharSequence value, final int offset, final int len) throws IOException {
136         int start = offset;
137         int pos = offset;
138         final int end = offset + len;
139 
140         final char delim = format.getDelimiter();
141         final char escape = format.getEscapeCharacter().charValue();
142 
143         while (pos < end) {
144             char c = value.charAt(pos);
145             if (c == CR || c == LF || c == delim || c == escape) {
146                 // write out segment up until this char
147                 if (pos > start) {
148                     out.append(value, start, pos);
149                 }
150                 if (c == LF) {
151                     c = 'n';
152                 } else if (c == CR) {
153                     c = 'r';
154                 }
155 
156                 out.append(escape);
157                 out.append(c);
158 
159                 start = pos + 1; // start on the current char after this one
160             }
161 
162             pos++;
163         }
164 
165         // write last segment
166         if (pos > start) {
167             out.append(value, start, pos);
168         }
169     }
170 
171     /*
172      * Note: must only be called if quoting is enabled, otherwise will generate NPE
173      */
174     // the original object is needed so can check for Number
175     private void printAndQuote(final Object object, final CharSequence value,
176             final int offset, final int len) throws IOException {
177         boolean quote = false;
178         int start = offset;
179         int pos = offset;
180         final int end = offset + len;
181 
182         final char delimChar = format.getDelimiter();
183         final char quoteChar = format.getQuoteCharacter().charValue();
184 
185         QuoteMode quoteModePolicy = format.getQuoteMode();
186         if (quoteModePolicy == null) {
187             quoteModePolicy = QuoteMode.MINIMAL;
188         }
189         switch (quoteModePolicy) {
190         case ALL:
191             quote = true;
192             break;
193         case NON_NUMERIC:
194             quote = !(object instanceof Number);
195             break;
196         case NONE:
197             // Use the existing escaping code
198             printAndEscape(value, offset, len);
199             return;
200         case MINIMAL:
201             if (len <= 0) {
202                 // always quote an empty token that is the first
203                 // on the line, as it may be the only thing on the
204                 // line. If it were not quoted in that case,
205                 // an empty line has no tokens.
206                 if (newRecord) {
207                     quote = true;
208                 }
209             } else {
210                 char c = value.charAt(pos);
211 
212                 // TODO where did this rule come from?
213                 if (newRecord && (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z'))) {
214                     quote = true;
215                 } else if (c <= COMMENT) {
216                     // Some other chars at the start of a value caused the parser to fail, so for now
217                     // encapsulate if we start in anything less than '#'. We are being conservative
218                     // by including the default comment char too.
219                     quote = true;
220                 } else {
221                     while (pos < end) {
222                         c = value.charAt(pos);
223                         if (c == LF || c == CR || c == quoteChar || c == delimChar) {
224                             quote = true;
225                             break;
226                         }
227                         pos++;
228                     }
229 
230                     if (!quote) {
231                         pos = end - 1;
232                         c = value.charAt(pos);
233                         // Some other chars at the end caused the parser to fail, so for now
234                         // encapsulate if we end in anything less than ' '
235                         if (c <= SP) {
236                             quote = true;
237                         }
238                     }
239                 }
240             }
241 
242             if (!quote) {
243                 // no encapsulation needed - write out the original value
244                 out.append(value, start, end);
245                 return;
246             }
247             break;
248         default:
249             throw new IllegalStateException("Unexpected Quote value: " + quoteModePolicy);
250         }
251 
252         if (!quote) {
253             // no encapsulation needed - write out the original value
254             out.append(value, start, end);
255             return;
256         }
257 
258         // we hit something that needed encapsulation
259         out.append(quoteChar);
260 
261         // Pick up where we left off: pos should be positioned on the first character that caused
262         // the need for encapsulation.
263         while (pos < end) {
264             final char c = value.charAt(pos);
265             if (c == quoteChar) {
266                 // write out the chunk up until this point
267 
268                 // add 1 to the length to write out the encapsulator also
269                 out.append(value, start, pos + 1);
270                 // put the next starting position on the encapsulator so we will
271                 // write it out again with the next string (effectively doubling it)
272                 start = pos;
273             }
274             pos++;
275         }
276 
277         // write the last segment
278         out.append(value, start, pos);
279         out.append(quoteChar);
280     }
281 
282     /**
283      * Prints a comment on a new line among the delimiter separated values.
284      *
285      * <p>
286      * Comments will always begin on a new line and occupy a least one full line. The character specified to start
287      * comments and a space will be inserted at the beginning of each new line in the comment.
288      * </p>
289      *
290      * If comments are disabled in the current CSV format this method does nothing.
291      *
292      * @param comment
293      *            the comment to output
294      * @throws IOException
295      *             If an I/O error occurs
296      */
297     public void printComment(final String comment) throws IOException {
298         if (!format.isCommentMarkerSet()) {
299             return;
300         }
301         if (!newRecord) {
302             println();
303         }
304         out.append(format.getCommentMarker().charValue());
305         out.append(SP);
306         for (int i = 0; i < comment.length(); i++) {
307             final char c = comment.charAt(i);
308             switch (c) {
309             case CR:
310                 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
311                     i++;
312                 }
313                 //$FALL-THROUGH$ break intentionally excluded.
314             case LF:
315                 println();
316                 out.append(format.getCommentMarker().charValue());
317                 out.append(SP);
318                 break;
319             default:
320                 out.append(c);
321                 break;
322             }
323         }
324         println();
325     }
326 
327     /**
328      * Outputs the record separator.
329      *
330      * @throws IOException
331      *             If an I/O error occurs
332      */
333     public void println() throws IOException {
334         final String recordSeparator = format.getRecordSeparator();
335         if (recordSeparator != null) {
336             out.append(recordSeparator);
337         }
338         newRecord = true;
339     }
340 
341     /**
342      * Prints the given values a single record of delimiter separated values followed by the record separator.
343      *
344      * <p>
345      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
346      * separator to the output after printing the record, so there is no need to call {@link #println()}.
347      * </p>
348      *
349      * @param values
350      *            values to output.
351      * @throws IOException
352      *             If an I/O error occurs
353      */
354     public void printRecord(final Iterable<?> values) throws IOException {
355         for (final Object value : values) {
356             print(value);
357         }
358         println();
359     }
360 
361     /**
362      * Prints the given values a single record of delimiter separated values followed by the record separator.
363      *
364      * <p>
365      * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
366      * separator to the output after printing the record, so there is no need to call {@link #println()}.
367      * </p>
368      *
369      * @param values
370      *            values to output.
371      * @throws IOException
372      *             If an I/O error occurs
373      */
374     public void printRecord(final Object... values) throws IOException {
375         for (final Object value : values) {
376             print(value);
377         }
378         println();
379     }
380 
381     /**
382      * Prints all the objects in the given collection handling nested collections/arrays as records.
383      *
384      * <p>If the given collection only contains simple objects, this method will print a single record like
385      * {@link #printRecord(Iterable)}. If the given collections contains nested collections/arrays those nested elements
386      * will each be printed as records using {@link #printRecord(Object...)}.</p>
387      *
388      * <p>Given the following data structure:</p>
389      * <pre>
390      * <code>
391      * List&lt;String[]&gt; data = ...
392      * data.add(new String[]{ "A", "B", "C" });
393      * data.add(new String[]{ "1", "2", "3" });
394      * data.add(new String[]{ "A1", "B2", "C3" });
395      * </code>
396      * </pre>
397      *
398      * <p>Calling this method will print:</p>
399      * <pre>
400      * <code>
401      * A, B, C
402      * 1, 2, 3
403      * A1, B2, C3
404      * </code>
405      * </pre>
406      *
407      * @param values
408      *            the values to print.
409      * @throws IOException
410      *             If an I/O error occurs
411      */
412     public void printRecords(final Iterable<?> values) throws IOException {
413         for (final Object value : values) {
414             if (value instanceof Object[]) {
415                 this.printRecord((Object[]) value);
416             } else if (value instanceof Iterable) {
417                 this.printRecord((Iterable<?>) value);
418             } else {
419                 this.printRecord(value);
420             }
421         }
422     }
423 
424     /**
425      * Prints all the objects in the given array handling nested collections/arrays as records.
426      *
427      * <p>If the given array only contains simple objects, this method will print a single record like
428      * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
429      * elements will each be printed as records using {@link #printRecord(Object...)}.</p>
430      *
431      * <p>Given the following data structure:</p>
432      * <pre>
433      * <code>
434      * String[][] data = new String[3][]
435      * data[0] = String[]{ "A", "B", "C" };
436      * data[1] = new String[]{ "1", "2", "3" };
437      * data[2] = new String[]{ "A1", "B2", "C3" };
438      * </code>
439      * </pre>
440      *
441      * <p>Calling this method will print:</p>
442      * <pre>
443      * <code>
444      * A, B, C
445      * 1, 2, 3
446      * A1, B2, C3
447      * </code>
448      * </pre>
449      *
450      * @param values
451      *            the values to print.
452      * @throws IOException
453      *             If an I/O error occurs
454      */
455     public void printRecords(final Object... values) throws IOException {
456         for (final Object value : values) {
457             if (value instanceof Object[]) {
458                 this.printRecord((Object[]) value);
459             } else if (value instanceof Iterable) {
460                 this.printRecord((Iterable<?>) value);
461             } else {
462                 this.printRecord(value);
463             }
464         }
465     }
466 
467     /**
468      * Prints all the objects in the given JDBC result set.
469      *
470      * @param resultSet result set
471      *            the values to print.
472      * @throws IOException
473      *             If an I/O error occurs
474      * @throws SQLException if a database access error occurs
475      */
476     public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
477         final int columnCount = resultSet.getMetaData().getColumnCount();
478         while (resultSet.next()) {
479             for (int i = 1; i <= columnCount; i++) {
480                 print(resultSet.getString(i));
481             }
482             println();
483         }
484     }
485 
486     /**
487      * Gets the target Appendable.
488      *
489      * @return the target Appendable.
490      */
491     public Appendable getOut() {
492         return this.out;
493     }
494 }