View Javadoc

1   /*
2    * Java CSV is a stream based library for reading and writing
3    * CSV and other delimited data.
4    *   
5    * Copyright (C) Bruce Dunwiddie bruce@csvreader.com
6    *
7    * This library is free software; you can redistribute it and/or
8    * modify it under the terms of the GNU Lesser General Public
9    * License as published by the Free Software Foundation; either
10   * version 2.1 of the License, or (at your option) any later version.
11   *
12   * This library is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   * Lesser General Public License for more details.
16   *
17   * You should have received a copy of the GNU Lesser General Public
18   * License along with this library; if not, write to the Free Software
19   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
20   */
21  package com.csvreader;
22  
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.OutputStream;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintWriter;
28  import java.io.Writer;
29  import java.nio.charset.Charset;
30  
31  /***
32   * A stream based writer for writing delimited text data to a file or a stream.
33   */
34  public class CsvWriter {
35      private PrintWriter outputStream = null;
36  
37      private String fileName = null;
38  
39      private boolean firstColumn = true;
40  
41      private boolean useCustomRecordDelimiter = false;
42  
43      private Charset charset = null;
44  
45      // this holds all the values for switches that the user is allowed to set
46      private UserSettings userSettings = new UserSettings();
47  
48      private boolean initialized = false;
49  
50      private boolean closed = false;
51  
52      /***
53       * Double up the text qualifier to represent an occurance of the text
54       * qualifier.
55       */
56      public static final int ESCAPE_MODE_DOUBLED = 1;
57  
58      /***
59       * Use a backslash character before the text qualifier to represent an
60       * occurance of the text qualifier.
61       */
62      public static final int ESCAPE_MODE_BACKSLASH = 2;
63  
64      /***
65       * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using a file
66       * as the data destination.
67       * 
68       * @param fileName
69       *                The path to the file to output the data.
70       * @param delimiter
71       *                The character to use as the column delimiter.
72       * @param charset
73       *                The {@link java.nio.charset.Charset Charset} to use while
74       *                writing the data.
75       */
76      public CsvWriter(String fileName, char delimiter, Charset charset) {
77          if (fileName == null) {
78              throw new IllegalArgumentException("Parameter fileName can not be null.");
79          }
80  
81          if (charset == null) {
82              throw new IllegalArgumentException("Parameter charset can not be null.");
83          }
84  
85          this.fileName = fileName;
86          userSettings.Delimiter = delimiter;
87          this.charset = charset;
88      }
89  
90      /***
91       * Creates a {@link com.csvreader.CsvWriter CsvWriter} object using a file
92       * as the data destination. Uses a comma as the column delimiter and
93       * ISO-8859-1 as the {@link java.nio.charset.Charset Charset}.
94       * 
95       * @param fileName
96       *                The path to the file to output the data.
97       */
98      public CsvWriter(String fileName) {
99          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
166      *                Windows, 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
256      *                this 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) throws IOException {
262         checkClosed();
263 
264         checkInit();
265 
266         // BEGIN <erilong@users.sourceforge.net>
267         // if (content == null) {
268         // content = "";
269         // }
270         // END
271 
272         if (!firstColumn) {
273             outputStream.write(userSettings.Delimiter);
274         }
275 
276         boolean textQualify = userSettings.ForceQualifier;
277 
278         // BEGIN <erilong@users.sourceforge.net>
279         // We want a null to be an empty unquoted element
280         if (content == null) {
281             content = "";
282             textQualify = false;
283         }
284         // We want an empty string to be a quoted element
285         else if (content.equals("")) {
286             textQualify = true;
287         }
288         // END
289 
290         if (!preserveSpaces && content.length() > 0) {
291             content = content.trim();
292         }
293 
294         if (!textQualify
295                 && userSettings.UseTextQualifier
296                 && (content.indexOf(userSettings.TextQualifier) > -1
297                         || content.indexOf(userSettings.Delimiter) > -1
298                         || (!useCustomRecordDelimiter && (content.indexOf(Letters.LF) > -1 || content
299                                 .indexOf(Letters.CR) > -1))
300                         || (useCustomRecordDelimiter && content.indexOf(userSettings.RecordDelimiter) > -1)
301                         || (firstColumn && content.length() > 0 && content.charAt(0) == userSettings.Comment) ||
302                 // check for empty first column, which if on its own line must
303                 // be qualified or the line will be skipped
304                 (firstColumn && content.length() == 0))) {
305             textQualify = true;
306         }
307 
308         if (userSettings.UseTextQualifier && !textQualify && content.length() > 0 && preserveSpaces) {
309             char firstLetter = content.charAt(0);
310 
311             if (firstLetter == Letters.SPACE || firstLetter == Letters.TAB) {
312                 textQualify = true;
313             }
314 
315             if (!textQualify && content.length() > 1) {
316                 char lastLetter = content.charAt(content.length() - 1);
317 
318                 if (lastLetter == Letters.SPACE || lastLetter == Letters.TAB) {
319                     textQualify = true;
320                 }
321             }
322         }
323 
324         if (textQualify) {
325             outputStream.write(userSettings.TextQualifier);
326 
327             if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
328                 content = replace(content, "" + Letters.BACKSLASH, "" + Letters.BACKSLASH + Letters.BACKSLASH);
329                 content = replace(content, "" + userSettings.TextQualifier, "" + Letters.BACKSLASH
330                         + userSettings.TextQualifier);
331             } else {
332                 content = replace(content, "" + userSettings.TextQualifier, "" + userSettings.TextQualifier
333                         + userSettings.TextQualifier);
334             }
335         } else if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
336             content = replace(content, "" + Letters.BACKSLASH, "" + Letters.BACKSLASH + Letters.BACKSLASH);
337             content = replace(content, "" + userSettings.Delimiter, "" + Letters.BACKSLASH + userSettings.Delimiter);
338 
339             if (useCustomRecordDelimiter) {
340                 content = replace(content, "" + userSettings.RecordDelimiter, "" + Letters.BACKSLASH
341                         + userSettings.RecordDelimiter);
342             } else {
343                 content = replace(content, "" + Letters.CR, "" + Letters.BACKSLASH + Letters.CR);
344                 content = replace(content, "" + Letters.LF, "" + Letters.BACKSLASH + Letters.LF);
345             }
346 
347             if (firstColumn && content.length() > 0 && content.charAt(0) == userSettings.Comment) {
348                 if (content.length() > 1) {
349                     content = "" + Letters.BACKSLASH + userSettings.Comment + content.substring(1);
350                 } else {
351                     content = "" + Letters.BACKSLASH + userSettings.Comment;
352                 }
353             }
354         }
355 
356         outputStream.write(content);
357 
358         if (textQualify) {
359             outputStream.write(userSettings.TextQualifier);
360         }
361 
362         firstColumn = false;
363     }
364 
365     /***
366      * Writes another column of data to this record.&nbsp;Does not preserve
367      * leading and trailing whitespace in this column of data.
368      * 
369      * @param content
370      *                The data for the new column.
371      * @exception IOException
372      *                    Thrown if an error occurs while writing data to the
373      *                    destination stream.
374      */
375     public void write(String content) throws IOException {
376         write(content, false);
377     }
378 
379     public void writeComment(String commentText) throws IOException {
380         checkClosed();
381 
382         checkInit();
383 
384         outputStream.write(userSettings.Comment);
385 
386         outputStream.write(commentText);
387 
388         if (useCustomRecordDelimiter) {
389             outputStream.write(userSettings.RecordDelimiter);
390         } else {
391             outputStream.println();
392         }
393 
394         firstColumn = true;
395     }
396 
397     /***
398      * Writes a new record using the passed in array of values.
399      * 
400      * @param values
401      *                Values to be written.
402      * 
403      * @param preserveSpaces
404      *                Whether to preserver leading and trailing spaces in
405      *                columns while writing out to the record or not.
406      * 
407      * @throws IOException
408      *                 Thrown if an error occurs while writing data to the
409      *                 destination stream.
410      */
411     public void writeRecord(String[] values, boolean preserveSpaces) 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(new FileOutputStream(fileName), charset));
463             }
464 
465             initialized = true;
466         }
467     }
468 
469     /***
470      * Clears all buffers for the current writer and causes any buffered data to
471      * be written to the underlying device.
472      */
473     public void flush() {
474         outputStream.flush();
475     }
476 
477     /***
478      * Closes and releases all related resources.
479      */
480     public void close() {
481         if (!closed) {
482             close(true);
483 
484             closed = true;
485         }
486     }
487 
488     /***
489      * 
490      */
491     private void close(boolean closing) {
492         if (!closed) {
493             if (closing) {
494                 charset = null;
495             }
496 
497             try {
498                 if (initialized) {
499                     outputStream.close();
500                 }
501             } catch (Exception e) {
502                 // just eat the exception
503             }
504 
505             outputStream = null;
506 
507             closed = true;
508         }
509     }
510 
511     /***
512      * 
513      */
514     private void checkClosed() throws IOException {
515         if (closed) {
516             throw new IOException("This instance of the CsvWriter class has already been closed.");
517         }
518     }
519 
520     /***
521      * 
522      */
523     protected void finalize() {
524         close(false);
525     }
526 
527     private class Letters {
528         public static final char LF = '\n';
529 
530         public static final char CR = '\r';
531 
532         public static final char QUOTE = '"';
533 
534         public static final char COMMA = ',';
535 
536         public static final char SPACE = ' ';
537 
538         public static final char TAB = '\t';
539 
540         public static final char POUND = '#';
541 
542         public static final char BACKSLASH = '//';
543 
544         public static final char NULL = '\0';
545     }
546 
547     private class UserSettings {
548         // having these as publicly accessible members will prevent
549         // the overhead of the method call that exists on properties
550         public char TextQualifier;
551 
552         public boolean UseTextQualifier;
553 
554         public char Delimiter;
555 
556         public char RecordDelimiter;
557 
558         public char Comment;
559 
560         public int EscapeMode;
561 
562         public boolean ForceQualifier;
563 
564         public UserSettings() {
565             TextQualifier = Letters.QUOTE;
566             UseTextQualifier = true;
567             Delimiter = Letters.COMMA;
568             RecordDelimiter = Letters.NULL;
569             Comment = Letters.POUND;
570             EscapeMode = ESCAPE_MODE_DOUBLED;
571             ForceQualifier = false;
572         }
573     }
574 
575     public static String replace(String original, String pattern, String replace) {
576         final int len = pattern.length();
577         int found = original.indexOf(pattern);
578 
579         if (found > -1) {
580             StringBuffer sb = new StringBuffer();
581             int start = 0;
582 
583             while (found != -1) {
584                 sb.append(original.substring(start, found));
585                 sb.append(replace);
586                 start = found + len;
587                 found = original.indexOf(pattern, start);
588             }
589 
590             sb.append(original.substring(start));
591 
592             return sb.toString();
593         } else {
594             return original;
595         }
596     }
597 }