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