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 1592365 2014-05-04 16:08:56Z 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
49       * (encapsulation 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 IllegalArgumentException
57       *             thrown if the parameters of the format are inconsistent or if either out or format are null.
58       */
59      public CSVPrinter(final Appendable out, final CSVFormat format) {
60          Assertions.notNull(out, "out");
61          Assertions.notNull(format, "format");
62  
63          this.out = out;
64          this.format = format;
65          this.format.validate();
66      }
67  
68      // ======================================================
69      // printing implementation
70      // ======================================================
71  
72      public void close() throws IOException {
73          if (out instanceof Closeable) {
74              ((Closeable) out).close();
75          }
76      }
77  
78      /**
79       * Flushes the underlying stream.
80       *
81       * @throws IOException
82       *             If an I/O error occurs
83       */
84      public void flush() throws IOException {
85          if (out instanceof Flushable) {
86              ((Flushable) out).flush();
87          }
88      }
89  
90      /**
91       * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
92       *
93       * @param value
94       *            value to be output.
95       * @throws IOException
96       *             If an I/O error occurs
97       */
98      public void print(final Object value) throws IOException {
99          // null values are considered empty
100         String strValue;
101         if (value == null) {
102             final String nullString = format.getNullString();
103             strValue = nullString == null ? Constants.EMPTY : nullString;
104         } else {
105             strValue = value.toString();
106         }
107         this.print(value, strValue, 0, strValue.length());
108     }
109 
110     private void print(final Object object, final CharSequence value,
111             final int offset, final int len) throws IOException {
112         if (!newRecord) {
113             out.append(format.getDelimiter());
114         }
115         if (format.isQuoting()) {
116             // the original object is needed so can check for Number
117             printAndQuote(object, value, offset, len);
118         } else if (format.isEscaping()) {
119             printAndEscape(value, offset, len);
120         } else {
121             out.append(value, offset, offset + len);
122         }
123         newRecord = false;
124     }
125 
126     /*
127      * Note: must only be called if escaping is enabled, otherwise will generate NPE
128      */
129     private void printAndEscape(final CharSequence value, final int offset, final int len) throws IOException {
130         int start = offset;
131         int pos = offset;
132         final int end = offset + len;
133 
134         final char delim = format.getDelimiter();
135         final char escape = format.getEscape().charValue();
136 
137         while (pos < end) {
138             char c = value.charAt(pos);
139             if (c == CR || c == LF || c == delim || c == escape) {
140                 // write out segment up until this char
141                 if (pos > start) {
142                     out.append(value, start, pos);
143                 }
144                 if (c == LF) {
145                     c = 'n';
146                 } else if (c == CR) {
147                     c = 'r';
148                 }
149 
150                 out.append(escape);
151                 out.append(c);
152 
153                 start = pos + 1; // start on the current char after this one
154             }
155 
156             pos++;
157         }
158 
159         // write last segment
160         if (pos > start) {
161             out.append(value, start, pos);
162         }
163     }
164 
165     /*
166      * Note: must only be called if quoting is enabled, otherwise will generate NPE
167      */
168     // the original object is needed so can check for Number
169     private void printAndQuote(final Object object, final CharSequence value,
170             final int offset, final int len) throws IOException {
171         boolean quote = false;
172         int start = offset;
173         int pos = offset;
174         final int end = offset + len;
175 
176         final char delimChar = format.getDelimiter();
177         final char quoteChar = format.getQuoteChar().charValue();
178 
179         Quote quotePolicy = format.getQuotePolicy();
180         if (quotePolicy == null) {
181             quotePolicy = Quote.MINIMAL;
182         }
183         switch (quotePolicy) {
184         case ALL:
185             quote = true;
186             break;
187         case NON_NUMERIC:
188             quote = !(object instanceof Number);
189             break;
190         case NONE:
191             // Use the existing escaping code
192             printAndEscape(value, offset, len);
193             return;
194         case MINIMAL:
195             if (len <= 0) {
196                 // always quote an empty token that is the first
197                 // on the line, as it may be the only thing on the
198                 // line. If it were not quoted in that case,
199                 // an empty line has no tokens.
200                 if (newRecord) {
201                     quote = true;
202                 }
203             } else {
204                 char c = value.charAt(pos);
205 
206                 // Hmmm, where did this rule come from?
207                 if (newRecord && (c < '0' || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z'))) {
208                     quote = true;
209                     // } else if (c == ' ' || c == '\f' || c == '\t') {
210                 } else if (c <= COMMENT) {
211                     // Some other chars at the start of a value caused the parser to fail, so for now
212                     // encapsulate if we start in anything less than '#'. We are being conservative
213                     // by including the default comment char too.
214                     quote = true;
215                 } else {
216                     while (pos < end) {
217                         c = value.charAt(pos);
218                         if (c == LF || c == CR || c == quoteChar || c == delimChar) {
219                             quote = true;
220                             break;
221                         }
222                         pos++;
223                     }
224 
225                     if (!quote) {
226                         pos = end - 1;
227                         c = value.charAt(pos);
228                         // if (c == ' ' || c == '\f' || c == '\t') {
229                         // Some other chars at the end caused the parser to fail, so for now
230                         // encapsulate if we end in anything less than ' '
231                         if (c <= SP) {
232                             quote = true;
233                         }
234                     }
235                 }
236             }
237 
238             if (!quote) {
239                 // no encapsulation needed - write out the original value
240                 out.append(value, start, end);
241                 return;
242             }
243             break;
244         default:
245             throw new IllegalStateException("Unexpected Quote value: " + quotePolicy);
246         }
247 
248         if (!quote) {
249             // no encapsulation needed - write out the original value
250             out.append(value, start, end);
251             return;
252         }
253 
254         // we hit something that needed encapsulation
255         out.append(quoteChar);
256 
257         // Pick up where we left off: pos should be positioned on the first character that caused
258         // the need for encapsulation.
259         while (pos < end) {
260             final char c = value.charAt(pos);
261             if (c == quoteChar) {
262                 // write out the chunk up until this point
263 
264                 // add 1 to the length to write out the encapsulator also
265                 out.append(value, start, pos + 1);
266                 // put the next starting position on the encapsulator so we will
267                 // write it out again with the next string (effectively doubling it)
268                 start = pos;
269             }
270             pos++;
271         }
272 
273         // write the last segment
274         out.append(value, start, pos);
275         out.append(quoteChar);
276     }
277 
278     /**
279      * Prints a comment on a new line among the delimiter separated values.
280      *
281      * <p>
282      * Comments will always begin on a new line and occupy a least one full line. The character specified to start
283      * comments and a space will be inserted at the beginning of each new line in the comment.
284      * </p>
285      *
286      * If comments are disabled in the current CSV format this method does nothing.
287      *
288      * @param comment
289      *            the comment to output
290      * @throws IOException
291      *             If an I/O error occurs
292      */
293     public void printComment(final String comment) throws IOException {
294         if (!format.isCommentingEnabled()) {
295             return;
296         }
297         if (!newRecord) {
298             println();
299         }
300         out.append(format.getCommentStart().charValue());
301         out.append(SP);
302         for (int i = 0; i < comment.length(); i++) {
303             final char c = comment.charAt(i);
304             switch (c) {
305             case CR:
306                 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
307                     i++;
308                 }
309                 //$FALL-THROUGH$ break intentionally excluded.
310             case LF:
311                 println();
312                 out.append(format.getCommentStart().charValue());
313                 out.append(SP);
314                 break;
315             default:
316                 out.append(c);
317                 break;
318             }
319         }
320         println();
321     }
322 
323     /**
324      * Outputs the record separator.
325      *
326      * @throws IOException
327      *             If an I/O error occurs
328      */
329     public void println() throws IOException {
330         final String recordSeparator = format.getRecordSeparator();
331         if (recordSeparator != null) {
332             out.append(recordSeparator);
333         }
334         newRecord = true;
335     }
336 
337     /**
338      * Prints a single line of delimiter separated values. The values will be quoted if needed. Quotes and newLine
339      * characters will be escaped.
340      *
341      * @param values
342      *            values to output.
343      * @throws IOException
344      *             If an I/O error occurs
345      */
346     public void printRecord(final Iterable<?> values) throws IOException {
347         for (final Object value : values) {
348             print(value);
349         }
350         println();
351     }
352 
353     /**
354      * Prints a single line of delimiter separated values. The values will be quoted if needed. Quotes and newLine
355      * characters will be escaped.
356      *
357      * @param values
358      *            values to output.
359      * @throws IOException
360      *             If an I/O error occurs
361      */
362     public void printRecord(final Object... values) throws IOException {
363         for (final Object value : values) {
364             print(value);
365         }
366         println();
367     }
368 
369     /**
370      * Prints all the objects in the given collection.
371      *
372      * @param values
373      *            the values to print.
374      * @throws IOException
375      *             If an I/O error occurs
376      */
377     public void printRecords(final Iterable<?> values) throws IOException {
378         for (final Object value : values) {
379             if (value instanceof Object[]) {
380                 this.printRecord((Object[]) value);
381             } else if (value instanceof Iterable) {
382                 this.printRecord((Iterable<?>) value);
383             } else {
384                 this.printRecord(value);
385             }
386         }
387     }
388 
389     /**
390      * Prints all the objects in the given array.
391      *
392      * @param values
393      *            the values to print.
394      * @throws IOException
395      *             If an I/O error occurs
396      */
397     public void printRecords(final Object[] values) throws IOException {
398         for (final Object value : values) {
399             if (value instanceof Object[]) {
400                 this.printRecord((Object[]) value);
401             } else if (value instanceof Iterable) {
402                 this.printRecord((Iterable<?>) value);
403             } else {
404                 this.printRecord(value);
405             }
406         }
407     }
408 
409     /**
410      * Prints all the objects in the given JDBC result set.
411      *
412      * @param resultSet result set
413      *            the values to print.
414      * @throws IOException
415      *             If an I/O error occurs
416      * @throws SQLException if a database access error occurs
417      */
418     public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
419         final int columnCount = resultSet.getMetaData().getColumnCount();
420         while (resultSet.next()) {
421             for (int i = 1; i <= columnCount; i++) {
422                 print(resultSet.getString(i));
423             }
424             println();
425         }
426     }
427 
428     /**
429      * Gets the target Appendable.
430      *
431      * @return the target Appendable.
432      */
433     public Appendable getOut() {
434         return this.out;
435     }
436 }