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}