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 }