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