1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * https://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20 package org.apache.commons.csv; 21 22 import static org.apache.commons.csv.Constants.CR; 23 import static org.apache.commons.csv.Constants.LF; 24 import static org.apache.commons.csv.Constants.SP; 25 26 import java.io.Closeable; 27 import java.io.Flushable; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.Reader; 31 import java.sql.Blob; 32 import java.sql.Clob; 33 import java.sql.ResultSet; 34 import java.sql.SQLException; 35 import java.sql.Statement; 36 import java.util.Arrays; 37 import java.util.Objects; 38 import java.util.concurrent.locks.ReentrantLock; 39 import java.util.stream.Stream; 40 41 import org.apache.commons.io.function.IOStream; 42 43 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 44 45 /** 46 * Prints values in a {@link CSVFormat CSV format}. 47 * 48 * <p>Values can be appended to the output by calling the {@link #print(Object)} method. 49 * Values are printed according to {@link String#valueOf(Object)}. 50 * To complete a record the {@link #println()} method has to be called. 51 * Comments can be appended by calling {@link #printComment(String)}. 52 * However a comment will only be written to the output if the {@link CSVFormat} supports comments. 53 * </p> 54 * 55 * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)} 56 * or {@link #printRecord(Iterable)}. 57 * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)} 58 * methods can be used to print several records at once. 59 * </p> 60 * 61 * <p>Example:</p> 62 * 63 * <pre> 64 * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) { 65 * printer.printRecord("id", "userName", "firstName", "lastName", "birthday"); 66 * printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15)); 67 * printer.println(); 68 * printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29)); 69 * } catch (IOException ex) { 70 * ex.printStackTrace(); 71 * } 72 * </pre> 73 * 74 * <p>This code will write the following to csv.txt:</p> 75 * <pre> 76 * id,userName,firstName,lastName,birthday 77 * 1,john73,John,Doe,1973-09-15 78 * 79 * 2,mary,Mary,Meyer,1985-03-29 80 * </pre> 81 */ 82 public final class CSVPrinter implements Flushable, Closeable { 83 84 /** The place that the values get written. */ 85 private final Appendable appendable; 86 87 private final CSVFormat format; 88 89 /** True if we just began a new record. */ 90 private boolean newRecord = true; 91 92 private long recordCount; 93 94 private final ReentrantLock lock = new ReentrantLock(); 95 96 /** 97 * Creates a printer that will print values to the given stream following the CSVFormat. 98 * <p> 99 * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation and escaping with a different 100 * character) are not supported. 101 * </p> 102 * 103 * @param appendable stream to which to print. Must not be null. 104 * @param format the CSV format. Must not be null. 105 * @throws IOException thrown if the optional header cannot be printed. 106 * @throws IllegalArgumentException thrown if the parameters of the format are inconsistent. 107 * @throws NullPointerException thrown if either parameters are null. 108 */ 109 public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException { 110 Objects.requireNonNull(appendable, "appendable"); 111 Objects.requireNonNull(format, "format"); 112 this.appendable = appendable; 113 this.format = format.copy(); 114 // TODO: Is it a good idea to do this here instead of on the first call to a print method? 115 // It seems a pain to have to track whether the header has already been printed or not. 116 final String[] headerComments = format.getHeaderComments(); 117 if (headerComments != null) { 118 for (final String line : headerComments) { 119 printComment(line); 120 } 121 } 122 if (format.getHeader() != null && !format.getSkipHeaderRecord()) { 123 this.printRecord((Object[]) format.getHeader()); 124 } 125 } 126 127 @Override 128 public void close() throws IOException { 129 close(false); 130 } 131 132 /** 133 * Closes the underlying stream with an optional flush first. 134 * 135 * @param flush whether to flush before the actual close. 136 * @throws IOException 137 * If an I/O error occurs 138 * @since 1.6 139 * @see CSVFormat#getAutoFlush() 140 */ 141 public void close(final boolean flush) throws IOException { 142 if (flush || format.getAutoFlush()) { 143 flush(); 144 } 145 if (appendable instanceof Closeable) { 146 ((Closeable) appendable).close(); 147 } 148 } 149 150 /** 151 * Prints the record separator and increments the record count. 152 * 153 * @throws IOException 154 * If an I/O error occurs 155 */ 156 @SuppressFBWarnings(value = "AT_NONATOMIC_OPERATIONS_ON_SHARED_VARIABLE", justification = "https://github.com/spotbugs/spotbugs/issues/3428") 157 private void endOfRecord() throws IOException { 158 println(); 159 recordCount++; 160 } 161 162 /** 163 * Flushes the underlying stream. 164 * 165 * @throws IOException 166 * If an I/O error occurs 167 */ 168 @Override 169 public void flush() throws IOException { 170 if (appendable instanceof Flushable) { 171 ((Flushable) appendable).flush(); 172 } 173 } 174 175 /** 176 * Gets the target Appendable. 177 * 178 * @return the target Appendable. 179 */ 180 public Appendable getOut() { 181 return appendable; 182 } 183 184 /** 185 * Gets the record count printed, this does not include comments or headers. 186 * 187 * @return the record count, this does not include comments or headers. 188 * @since 1.13.0 189 */ 190 public long getRecordCount() { 191 return recordCount; 192 } 193 194 /** 195 * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed. 196 * 197 * @param value 198 * value to be output. 199 * @throws IOException 200 * If an I/O error occurs 201 */ 202 public void print(final Object value) throws IOException { 203 lock.lock(); 204 try { 205 printRaw(value); 206 } finally { 207 lock.unlock(); 208 } 209 } 210 211 /** 212 * Prints a comment on a new line among the delimiter-separated values. 213 * 214 * <p> 215 * Comments will always begin on a new line and occupy at least one full line. The character specified to start 216 * comments and a space will be inserted at the beginning of each new line in the comment. 217 * </p> 218 * 219 * <p> 220 * If comments are disabled in the current CSV format this method does nothing. 221 * </p> 222 * 223 * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()} 224 * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use 225 * line breaks as record separators.</p> 226 * 227 * @param comment 228 * the comment to output 229 * @throws IOException 230 * If an I/O error occurs 231 */ 232 public void printComment(final String comment) throws IOException { 233 lock.lock(); 234 try { 235 if (comment == null || !format.isCommentMarkerSet()) { 236 return; 237 } 238 if (!newRecord) { 239 println(); 240 } 241 appendable.append(format.getCommentMarker().charValue()); // Explicit (un)boxing is intentional 242 appendable.append(SP); 243 for (int i = 0; i < comment.length(); i++) { 244 final char c = comment.charAt(i); 245 switch (c) { 246 case CR: 247 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) { 248 i++; 249 } 250 // falls-through: break intentionally excluded. 251 case LF: 252 println(); 253 appendable.append(format.getCommentMarker().charValue()); // Explicit (un)boxing is intentional 254 appendable.append(SP); 255 break; 256 default: 257 appendable.append(c); 258 break; 259 } 260 } 261 println(); 262 } finally { 263 lock.unlock(); 264 } 265 } 266 267 /** 268 * Prints headers for a result set based on its metadata. 269 * 270 * @param resultSet The ResultSet to query for metadata. 271 * @throws IOException If an I/O error occurs. 272 * @throws SQLException If a database access error occurs or this method is called on a closed result set. 273 * @since 1.9.0 274 */ 275 public void printHeaders(final ResultSet resultSet) throws IOException, SQLException { 276 lock.lock(); 277 try { 278 try (IOStream<String> stream = IOStream.of(format.builder().setHeader(resultSet).get().getHeader())) { 279 stream.forEachOrdered(this::print); 280 } 281 println(); 282 } finally { 283 lock.unlock(); 284 } 285 } 286 287 /** 288 * Prints the record separator. 289 * 290 * @throws IOException 291 * If an I/O error occurs 292 */ 293 public void println() throws IOException { 294 lock.lock(); 295 try { 296 format.println(appendable); 297 newRecord = true; 298 } finally { 299 lock.unlock(); 300 } 301 } 302 303 /** 304 * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed. 305 * 306 * @param value 307 * value to be output. 308 * @throws IOException 309 * If an I/O error occurs 310 */ 311 private void printRaw(final Object value) throws IOException { 312 format.print(value, appendable, newRecord); 313 newRecord = false; 314 } 315 316 /** 317 * Prints the given values as a single record of delimiter-separated values followed by the record separator. 318 * 319 * <p> 320 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 321 * separator to the output after printing the record, so there is no need to call {@link #println()}. 322 * </p> 323 * 324 * @param values 325 * values to output. 326 * @throws IOException 327 * If an I/O error occurs 328 */ 329 @SuppressWarnings("resource") 330 public void printRecord(final Iterable<?> values) throws IOException { 331 lock.lock(); 332 try { 333 IOStream.of(values).forEachOrdered(this::print); 334 endOfRecord(); 335 } finally { 336 lock.unlock(); 337 } 338 } 339 340 /** 341 * Prints the given values as a single record of delimiter-separated values followed by the record separator. 342 * 343 * <p> 344 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 345 * separator to the output after printing the record, so there is no need to call {@link #println()}. 346 * </p> 347 * 348 * @param values 349 * values to output. 350 * @throws IOException 351 * If an I/O error occurs 352 */ 353 public void printRecord(final Object... values) throws IOException { 354 printRecord(Arrays.asList(values)); 355 } 356 357 /** 358 * Prints the given values as a single record of delimiter-separated values followed by the record separator. 359 * 360 * <p> 361 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 362 * separator to the output after printing the record, so there is no need to call {@link #println()}. 363 * </p> 364 * 365 * @param stream 366 * values to output. 367 * @throws IOException 368 * If an I/O error occurs 369 * @since 1.10.0 370 */ 371 @SuppressWarnings("resource") // caller closes. 372 public void printRecord(final Stream<?> stream) throws IOException { 373 lock.lock(); 374 try { 375 IOStream.adapt(stream).forEachOrdered(stream.isParallel() ? this::printRaw : this::print); 376 endOfRecord(); 377 } finally { 378 lock.unlock(); 379 } 380 } 381 382 private void printRecordObject(final Object value) throws IOException { 383 if (value instanceof Object[]) { 384 this.printRecord((Object[]) value); 385 } else if (value instanceof Iterable) { 386 this.printRecord((Iterable<?>) value); 387 } else { 388 this.printRecord(value); 389 } 390 } 391 392 @SuppressWarnings("resource") 393 private void printRecords(final IOStream<?> stream) throws IOException { 394 format.limit(stream).forEachOrdered(this::printRecordObject); 395 } 396 397 /** 398 * Prints all the objects in the given {@link Iterable} handling nested collections/arrays as records. 399 * 400 * <p> 401 * If the given Iterable only contains simple objects, this method will print a single record like 402 * {@link #printRecord(Iterable)}. If the given Iterable contains nested collections/arrays those nested elements 403 * will each be printed as records using {@link #printRecord(Object...)}. 404 * </p> 405 * 406 * <p> 407 * Given the following data structure: 408 * </p> 409 * 410 * <pre>{@code 411 * List<String[]> data = new ArrayList<>(); 412 * data.add(new String[]{ "A", "B", "C" }); 413 * data.add(new String[]{ "1", "2", "3" }); 414 * data.add(new String[]{ "A1", "B2", "C3" }); 415 * } 416 * </pre> 417 * 418 * <p> 419 * Calling this method will print: 420 * </p> 421 * 422 * <pre> 423 * {@code 424 * A, B, C 425 * 1, 2, 3 426 * A1, B2, C3 427 * } 428 * </pre> 429 * 430 * @param values 431 * the values to print. 432 * @throws IOException 433 * If an I/O error occurs 434 */ 435 @SuppressWarnings("resource") 436 public void printRecords(final Iterable<?> values) throws IOException { 437 printRecords(IOStream.of(values)); 438 } 439 440 /** 441 * Prints all the objects in the given array handling nested collections/arrays as records. 442 * 443 * <p> 444 * If the given array only contains simple objects, this method will print a single record like 445 * {@link #printRecord(Object...)}. If the given collections contain nested collections or arrays, those nested 446 * elements will each be printed as records using {@link #printRecord(Object...)}. 447 * </p> 448 * 449 * <p> 450 * Given the following data structure: 451 * </p> 452 * 453 * <pre>{@code 454 * String[][] data = new String[3][] 455 * data[0] = String[]{ "A", "B", "C" }; 456 * data[1] = new String[]{ "1", "2", "3" }; 457 * data[2] = new String[]{ "A1", "B2", "C3" }; 458 * } 459 * </pre> 460 * 461 * <p> 462 * Calling this method will print: 463 * </p> 464 * 465 * <pre>{@code 466 * A, B, C 467 * 1, 2, 3 468 * A1, B2, C3 469 * } 470 * </pre> 471 * 472 * @param values 473 * the values to print. 474 * @throws IOException 475 * If an I/O error occurs 476 */ 477 public void printRecords(final Object... values) throws IOException { 478 printRecords(Arrays.asList(values)); 479 } 480 481 /** 482 * Prints all the objects in the given JDBC result set. 483 * <p> 484 * You can use {@link CSVFormat.Builder#setMaxRows(long)} to limit how many rows a result set produces. This is most useful when you cannot limit rows 485 * through {@link Statement#setLargeMaxRows(long)} or {@link Statement#setMaxRows(int)}. 486 * </p> 487 * 488 * @param resultSet The values to print. 489 * @throws IOException If an I/O error occurs. 490 * @throws SQLException Thrown when a database access error occurs. 491 */ 492 public void printRecords(final ResultSet resultSet) throws SQLException, IOException { 493 final int columnCount = resultSet.getMetaData().getColumnCount(); 494 while (resultSet.next() && format.useRow(resultSet.getRow())) { 495 lock.lock(); 496 try { 497 for (int i = 1; i <= columnCount; i++) { 498 final Object object = resultSet.getObject(i); 499 if (object instanceof Clob) { 500 try (Reader reader = ((Clob) object).getCharacterStream()) { 501 print(reader); 502 } 503 } else if (object instanceof Blob) { 504 try (InputStream inputStream = ((Blob) object).getBinaryStream()) { 505 print(inputStream); 506 } 507 } else { 508 print(object); 509 } 510 } 511 endOfRecord(); 512 } finally { 513 lock.unlock(); 514 } 515 } 516 } 517 518 /** 519 * Prints all the objects with metadata in the given JDBC result set based on the header boolean. 520 * <p> 521 * You can use {@link CSVFormat.Builder#setMaxRows(long)} to limit how many rows a result set produces. This is most useful when you cannot limit rows 522 * through {@link Statement#setLargeMaxRows(long)} or {@link Statement#setMaxRows(int)}. 523 * </p> 524 * 525 * @param resultSet source of row data. 526 * @param printHeader whether to print headers. 527 * @throws IOException If an I/O error occurs 528 * @throws SQLException if a database access error occurs 529 * @since 1.9.0 530 */ 531 public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException { 532 if (printHeader) { 533 printHeaders(resultSet); 534 } 535 printRecords(resultSet); 536 } 537 538 /** 539 * Prints all the objects in the given {@link Stream} handling nested collections/arrays as records. 540 * 541 * <p> 542 * If the given Stream only contains simple objects, this method will print a single record like 543 * {@link #printRecord(Iterable)}. If the given Stream contains nested collections/arrays those nested elements 544 * will each be printed as records using {@link #printRecord(Object...)}. 545 * </p> 546 * 547 * <p> 548 * Given the following data structure: 549 * </p> 550 * 551 * <pre>{@code 552 * List<String[]> data = new ArrayList<>(); 553 * data.add(new String[]{ "A", "B", "C" }); 554 * data.add(new String[]{ "1", "2", "3" }); 555 * data.add(new String[]{ "A1", "B2", "C3" }); 556 * Stream<String[]> stream = data.stream(); 557 * } 558 * </pre> 559 * 560 * <p> 561 * Calling this method will print: 562 * </p> 563 * 564 * <pre> 565 * {@code 566 * A, B, C 567 * 1, 2, 3 568 * A1, B2, C3 569 * } 570 * </pre> 571 * 572 * @param values 573 * the values to print. 574 * @throws IOException 575 * If an I/O error occurs 576 * @since 1.10.0 577 */ 578 @SuppressWarnings({ "resource" }) // Caller closes. 579 public void printRecords(final Stream<?> values) throws IOException { 580 printRecords(IOStream.adapt(values)); 581 } 582 }