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    }