001 /*
002 * Java CSV is a stream based library for reading and writing
003 * CSV and other delimited data.
004 *
005 * Copyright (C) Bruce Dunwiddie bruce@csvreader.com
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
020 */
021 package com.csvreader;
022
023 import java.io.FileOutputStream;
024 import java.io.IOException;
025 import java.io.OutputStream;
026 import java.io.OutputStreamWriter;
027 import java.io.PrintWriter;
028 import java.io.Writer;
029 import java.nio.charset.Charset;
030
031 /**
032 * A stream based writer for writing delimited text data to a file or a stream.
033 */
034 public class CsvWriter {
035 private PrintWriter outputStream = null;
036
037 private String fileName = null;
038
039 private boolean firstColumn = true;
040
041 private boolean useCustomRecordDelimiter = false;
042
043 private Charset charset = null;
044
045 // this holds all the values for switches that the user is allowed to set
046 private UserSettings userSettings = new UserSettings();
047
048 private boolean initialized = false;
049
050 private boolean closed = false;
051
052 /**
053 * Double up the text qualifier to represent an occurance of the text
054 * qualifier.
055 */
056 public static final int ESCAPE_MODE_DOUBLED = 1;
057
058 /**
059 * Use a backslash character before the text qualifier to represent an
060 * occurance of the text qualifier.
061 */
062 public static final int ESCAPE_MODE_BACKSLASH = 2;
063
064 /**
065 * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using a file
066 * as the data destination.
067 *
068 * @param fileName
069 * The path to the file to output the data.
070 * @param delimiter
071 * The character to use as the column delimiter.
072 * @param charset
073 * The {@link java.nio.charset.Charset Charset} to use while
074 * writing the data.
075 */
076 public CsvWriter(String fileName, char delimiter, Charset charset) {
077 if (fileName == null) {
078 throw new IllegalArgumentException("Parameter fileName can not be null.");
079 }
080
081 if (charset == null) {
082 throw new IllegalArgumentException("Parameter charset can not be null.");
083 }
084
085 this.fileName = fileName;
086 userSettings.Delimiter = delimiter;
087 this.charset = charset;
088 }
089
090 /**
091 * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using a file
092 * as the data destination. Uses a comma as the column delimiter and
093 * ISO-8859-1 as the {@link java.nio.charset.Charset Charset}.
094 *
095 * @param fileName
096 * The path to the file to output the data.
097 */
098 public CsvWriter(String fileName) {
099 this(fileName, Letters.COMMA, Charset.forName("ISO-8859-1"));
100 }
101
102 /**
103 * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using a Writer
104 * to write data to.
105 *
106 * @param outputStream
107 * The stream to write the column delimited data to.
108 * @param delimiter
109 * The character to use as the column delimiter.
110 */
111 public CsvWriter(Writer outputStream, char delimiter) {
112 if (outputStream == null) {
113 throw new IllegalArgumentException("Parameter outputStream can not be null.");
114 }
115
116 this.outputStream = new PrintWriter(outputStream);
117 userSettings.Delimiter = delimiter;
118 initialized = true;
119 }
120
121 /**
122 * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using an
123 * OutputStream to write data to.
124 *
125 * @param outputStream
126 * The stream to write the column delimited data to.
127 * @param delimiter
128 * The character to use as the column delimiter.
129 * @param charset
130 * The {@link java.nio.charset.Charset Charset} to use while
131 * writing the data.
132 */
133 public CsvWriter(OutputStream outputStream, char delimiter, Charset charset) {
134 this(new OutputStreamWriter(outputStream, charset), delimiter);
135 }
136
137 /**
138 * Gets the character being used as the column delimiter.
139 *
140 * @return The character being used as the column delimiter.
141 */
142 public char getDelimiter() {
143 return userSettings.Delimiter;
144 }
145
146 /**
147 * Sets the character to use as the column delimiter.
148 *
149 * @param delimiter
150 * The character to use as the column delimiter.
151 */
152 public void setDelimiter(char delimiter) {
153 userSettings.Delimiter = delimiter;
154 }
155
156 public char getRecordDelimiter() {
157 return userSettings.RecordDelimiter;
158 }
159
160 /**
161 * Sets the character to use as the record delimiter.
162 *
163 * @param recordDelimiter
164 * The character to use as the record delimiter. Default is
165 * combination of standard end of line characters for Windows,
166 * Unix, or Mac.
167 */
168 public void setRecordDelimiter(char recordDelimiter) {
169 useCustomRecordDelimiter = true;
170 userSettings.RecordDelimiter = recordDelimiter;
171 }
172
173 /**
174 * Gets the character to use as a text qualifier in the data.
175 *
176 * @return The character to use as a text qualifier in the data.
177 */
178 public char getTextQualifier() {
179 return userSettings.TextQualifier;
180 }
181
182 /**
183 * Sets the character to use as a text qualifier in the data.
184 *
185 * @param textQualifier
186 * The character to use as a text qualifier in the data.
187 */
188 public void setTextQualifier(char textQualifier) {
189 userSettings.TextQualifier = textQualifier;
190 }
191
192 /**
193 * Whether text qualifiers will be used while writing data or not.
194 *
195 * @return Whether text qualifiers will be used while writing data or not.
196 */
197 public boolean getUseTextQualifier() {
198 return userSettings.UseTextQualifier;
199 }
200
201 /**
202 * Sets whether text qualifiers will be used while writing data or not.
203 *
204 * @param useTextQualifier
205 * Whether to use a text qualifier while writing data or not.
206 */
207 public void setUseTextQualifier(boolean useTextQualifier) {
208 userSettings.UseTextQualifier = useTextQualifier;
209 }
210
211 public int getEscapeMode() {
212 return userSettings.EscapeMode;
213 }
214
215 public void setEscapeMode(int escapeMode) {
216 userSettings.EscapeMode = escapeMode;
217 }
218
219 public void setComment(char comment) {
220 userSettings.Comment = comment;
221 }
222
223 public char getComment() {
224 return userSettings.Comment;
225 }
226
227 /**
228 * Whether fields will be surrounded by the text qualifier even if the
229 * qualifier is not necessarily needed to escape this field.
230 *
231 * @return Whether fields will be forced to be qualified or not.
232 */
233 public boolean getForceQualifier() {
234 return userSettings.ForceQualifier;
235 }
236
237 /**
238 * Use this to force all fields to be surrounded by the text qualifier even
239 * if the qualifier is not necessarily needed to escape this field. Default
240 * is false.
241 *
242 * @param forceQualifier
243 * Whether to force the fields to be qualified or not.
244 */
245 public void setForceQualifier(boolean forceQualifier) {
246 userSettings.ForceQualifier = forceQualifier;
247 }
248
249 /**
250 * Writes another column of data to this record.
251 *
252 * @param content
253 * The data for the new column.
254 * @param preserveSpaces
255 * Whether to preserve leading and trailing whitespace in this
256 * column of data.
257 * @exception IOException
258 * Thrown if an error occurs while writing data to the
259 * destination stream.
260 */
261 public void write(String content, boolean preserveSpaces)
262 throws IOException {
263 checkClosed();
264
265 checkInit();
266
267 if (content == null) {
268 content = "";
269 }
270
271 if (!firstColumn) {
272 outputStream.write(userSettings.Delimiter);
273 }
274
275 boolean textQualify = userSettings.ForceQualifier;
276
277 if (!preserveSpaces && content.length() > 0) {
278 content = content.trim();
279 }
280
281 if (!textQualify
282 && userSettings.UseTextQualifier
283 && (content.indexOf(userSettings.TextQualifier) > -1
284 || content.indexOf(userSettings.Delimiter) > -1
285 || (!useCustomRecordDelimiter && (content
286 .indexOf(Letters.LF) > -1 || content
287 .indexOf(Letters.CR) > -1))
288 || (useCustomRecordDelimiter && content
289 .indexOf(userSettings.RecordDelimiter) > -1)
290 || (firstColumn && content.length() > 0 && content
291 .charAt(0) == userSettings.Comment) ||
292 // check for empty first column, which if on its own line must
293 // be qualified or the line will be skipped
294 (firstColumn && content.length() == 0))) {
295 textQualify = true;
296 }
297
298 if (userSettings.UseTextQualifier && !textQualify
299 && content.length() > 0 && preserveSpaces) {
300 char firstLetter = content.charAt(0);
301
302 if (firstLetter == Letters.SPACE || firstLetter == Letters.TAB) {
303 textQualify = true;
304 }
305
306 if (!textQualify && content.length() > 1) {
307 char lastLetter = content.charAt(content.length() - 1);
308
309 if (lastLetter == Letters.SPACE || lastLetter == Letters.TAB) {
310 textQualify = true;
311 }
312 }
313 }
314
315 if (textQualify) {
316 outputStream.write(userSettings.TextQualifier);
317
318 if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
319 content = replace(content, "" + Letters.BACKSLASH, ""
320 + Letters.BACKSLASH + Letters.BACKSLASH);
321 content = replace(content, "" + userSettings.TextQualifier, ""
322 + Letters.BACKSLASH + userSettings.TextQualifier);
323 } else {
324 content = replace(content, "" + userSettings.TextQualifier, ""
325 + userSettings.TextQualifier
326 + userSettings.TextQualifier);
327 }
328 } else if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
329 content = replace(content, "" + Letters.BACKSLASH, ""
330 + Letters.BACKSLASH + Letters.BACKSLASH);
331 content = replace(content, "" + userSettings.Delimiter, ""
332 + Letters.BACKSLASH + userSettings.Delimiter);
333
334 if (useCustomRecordDelimiter) {
335 content = replace(content, "" + userSettings.RecordDelimiter,
336 "" + Letters.BACKSLASH + userSettings.RecordDelimiter);
337 } else {
338 content = replace(content, "" + Letters.CR, ""
339 + Letters.BACKSLASH + Letters.CR);
340 content = replace(content, "" + Letters.LF, ""
341 + Letters.BACKSLASH + Letters.LF);
342 }
343
344 if (firstColumn && content.length() > 0
345 && content.charAt(0) == userSettings.Comment) {
346 if (content.length() > 1) {
347 content = "" + Letters.BACKSLASH + userSettings.Comment
348 + content.substring(1);
349 } else {
350 content = "" + Letters.BACKSLASH + userSettings.Comment;
351 }
352 }
353 }
354
355 outputStream.write(content);
356
357 if (textQualify) {
358 outputStream.write(userSettings.TextQualifier);
359 }
360
361 firstColumn = false;
362 }
363
364 /**
365 * Writes another column of data to this record. Does not preserve
366 * leading and trailing whitespace in this column of data.
367 *
368 * @param content
369 * The data for the new column.
370 * @exception IOException
371 * Thrown if an error occurs while writing data to the
372 * destination stream.
373 */
374 public void write(String content) throws IOException {
375 write(content, false);
376 }
377
378 public void writeComment(String commentText) throws IOException {
379 checkClosed();
380
381 checkInit();
382
383 outputStream.write(userSettings.Comment);
384
385 outputStream.write(commentText);
386
387 if (useCustomRecordDelimiter) {
388 outputStream.write(userSettings.RecordDelimiter);
389 } else {
390 outputStream.println();
391 }
392
393 firstColumn = true;
394 }
395
396 /**
397 * Writes a new record using the passed in array of values.
398 *
399 * @param values
400 * Values to be written.
401 *
402 * @param preserveSpaces
403 * Whether to preserver leading and trailing spaces in columns
404 * while writing out to the record or not.
405 *
406 * @throws IOException
407 * Thrown if an error occurs while writing data to the
408 * destination stream.
409 */
410 public void writeRecord(String[] values, boolean preserveSpaces)
411 throws IOException {
412 if (values != null && values.length > 0) {
413 for (int i = 0; i < values.length; i++) {
414 write(values[i], preserveSpaces);
415 }
416
417 endRecord();
418 }
419 }
420
421 /**
422 * Writes a new record using the passed in array of values.
423 *
424 * @param values
425 * Values to be written.
426 *
427 * @throws IOException
428 * Thrown if an error occurs while writing data to the
429 * destination stream.
430 */
431 public void writeRecord(String[] values) throws IOException {
432 writeRecord(values, false);
433 }
434
435 /**
436 * Ends the current record by sending the record delimiter.
437 *
438 * @exception IOException
439 * Thrown if an error occurs while writing data to the
440 * destination stream.
441 */
442 public void endRecord() throws IOException {
443 checkClosed();
444
445 checkInit();
446
447 if (useCustomRecordDelimiter) {
448 outputStream.write(userSettings.RecordDelimiter);
449 } else {
450 outputStream.println();
451 }
452
453 firstColumn = true;
454 }
455
456 /**
457 *
458 */
459 private void checkInit() throws IOException {
460 if (!initialized) {
461 if (fileName != null) {
462 outputStream = new PrintWriter(new OutputStreamWriter(
463 new FileOutputStream(fileName), charset));
464 }
465
466 initialized = true;
467 }
468 }
469
470 /**
471 * Clears all buffers for the current writer and causes any buffered data to
472 * be written to the underlying device.
473 */
474 public void flush() {
475 outputStream.flush();
476 }
477
478 /**
479 * Closes and releases all related resources.
480 */
481 public void close() {
482 if (!closed) {
483 close(true);
484
485 closed = true;
486 }
487 }
488
489 /**
490 *
491 */
492 private void close(boolean closing) {
493 if (!closed) {
494 if (closing) {
495 charset = null;
496 }
497
498 try {
499 if (initialized) {
500 outputStream.close();
501 }
502 } catch (Exception e) {
503 // just eat the exception
504 }
505
506 outputStream = null;
507
508 closed = true;
509 }
510 }
511
512 /**
513 *
514 */
515 private void checkClosed() throws IOException {
516 if (closed) {
517 throw new IOException(
518 "This instance of the CsvWriter class has already been closed.");
519 }
520 }
521
522 /**
523 *
524 */
525 protected void finalize() {
526 close(false);
527 }
528
529 private class Letters {
530 public static final char LF = '\n';
531
532 public static final char CR = '\r';
533
534 public static final char QUOTE = '"';
535
536 public static final char COMMA = ',';
537
538 public static final char SPACE = ' ';
539
540 public static final char TAB = '\t';
541
542 public static final char POUND = '#';
543
544 public static final char BACKSLASH = '\\';
545
546 public static final char NULL = '\0';
547 }
548
549 private class UserSettings {
550 // having these as publicly accessible members will prevent
551 // the overhead of the method call that exists on properties
552 public char TextQualifier;
553
554 public boolean UseTextQualifier;
555
556 public char Delimiter;
557
558 public char RecordDelimiter;
559
560 public char Comment;
561
562 public int EscapeMode;
563
564 public boolean ForceQualifier;
565
566 public UserSettings() {
567 TextQualifier = Letters.QUOTE;
568 UseTextQualifier = true;
569 Delimiter = Letters.COMMA;
570 RecordDelimiter = Letters.NULL;
571 Comment = Letters.POUND;
572 EscapeMode = ESCAPE_MODE_DOUBLED;
573 ForceQualifier = false;
574 }
575 }
576
577 public static String replace(String original, String pattern, String replace) {
578 final int len = pattern.length();
579 int found = original.indexOf(pattern);
580
581 if(found == -1) return original;
582
583 StringBuffer sb = new StringBuffer();
584 int start = 0;
585
586 while (found != -1) {
587 sb.append(original.substring(start, found));
588 sb.append(replace);
589 start = found + len;
590 found = original.indexOf(pattern, start);
591 }
592
593 sb.append(original.substring(start));
594
595 return sb.toString();
596 }
597 }