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