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