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 }