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