001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.csv; 019 020import static org.apache.commons.csv.Constants.CR; 021import static org.apache.commons.csv.Constants.LF; 022import static org.apache.commons.csv.Constants.SP; 023 024import java.io.Closeable; 025import java.io.Flushable; 026import java.io.IOException; 027import java.sql.Clob; 028import java.sql.ResultSet; 029import java.sql.SQLException; 030import java.util.Arrays; 031import java.util.Objects; 032import java.util.stream.Stream; 033 034/** 035 * Prints values in a {@link CSVFormat CSV format}. 036 * 037 * <p>Values can be appended to the output by calling the {@link #print(Object)} method. 038 * Values are printed according to {@link String#valueOf(Object)}. 039 * To complete a record the {@link #println()} method has to be called. 040 * Comments can be appended by calling {@link #printComment(String)}. 041 * However a comment will only be written to the output if the {@link CSVFormat} supports comments. 042 * </p> 043 * 044 * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)} 045 * or {@link #printRecord(Iterable)}. 046 * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)} 047 * methods can be used to print several records at once. 048 * </p> 049 * 050 * <p>Example:</p> 051 * 052 * <pre> 053 * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) { 054 * printer.printRecord("id", "userName", "firstName", "lastName", "birthday"); 055 * printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15)); 056 * printer.println(); 057 * printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29)); 058 * } catch (IOException ex) { 059 * ex.printStackTrace(); 060 * } 061 * </pre> 062 * 063 * <p>This code will write the following to csv.txt:</p> 064 * <pre> 065 * id,userName,firstName,lastName,birthday 066 * 1,john73,John,Doe,1973-09-15 067 * 068 * 2,mary,Mary,Meyer,1985-03-29 069 * </pre> 070 */ 071public final class CSVPrinter implements Flushable, Closeable { 072 073 /** The place that the values get written. */ 074 private final Appendable appendable; 075 076 private final CSVFormat format; 077 078 /** True if we just began a new record. */ 079 private boolean newRecord = true; 080 081 /** 082 * Creates a printer that will print values to the given stream following the CSVFormat. 083 * <p> 084 * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation 085 * and escaping with a different character) are not supported. 086 * </p> 087 * 088 * @param appendable 089 * stream to which to print. Must not be null. 090 * @param format 091 * the CSV format. Must not be null. 092 * @throws IOException 093 * thrown if the optional header cannot be printed. 094 * @throws IllegalArgumentException 095 * thrown if the parameters of the format are inconsistent or if either out or format are null. 096 */ 097 public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException { 098 Objects.requireNonNull(appendable, "appendable"); 099 Objects.requireNonNull(format, "format"); 100 101 this.appendable = appendable; 102 this.format = format.copy(); 103 // TODO: Is it a good idea to do this here instead of on the first call to a print method? 104 // It seems a pain to have to track whether the header has already been printed or not. 105 final String[] headerComments = format.getHeaderComments(); 106 if (headerComments != null) { 107 for (final String line : headerComments) { 108 this.printComment(line); 109 } 110 } 111 if (format.getHeader() != null && !format.getSkipHeaderRecord()) { 112 this.printRecord((Object[]) format.getHeader()); 113 } 114 } 115 116 @Override 117 public void close() throws IOException { 118 close(false); 119 } 120 121 /** 122 * Closes the underlying stream with an optional flush first. 123 * @param flush whether to flush before the actual close. 124 * 125 * @throws IOException 126 * If an I/O error occurs 127 * @since 1.6 128 */ 129 public void close(final boolean flush) throws IOException { 130 if (flush || format.getAutoFlush()) { 131 flush(); 132 } 133 if (appendable instanceof Closeable) { 134 ((Closeable) appendable).close(); 135 } 136 } 137 138 /** 139 * Flushes the underlying stream. 140 * 141 * @throws IOException 142 * If an I/O error occurs 143 */ 144 @Override 145 public void flush() throws IOException { 146 if (appendable instanceof Flushable) { 147 ((Flushable) appendable).flush(); 148 } 149 } 150 151 /** 152 * Gets the target Appendable. 153 * 154 * @return the target Appendable. 155 */ 156 public Appendable getOut() { 157 return this.appendable; 158 } 159 160 /** 161 * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed. 162 * 163 * @param value 164 * value to be output. 165 * @throws IOException 166 * If an I/O error occurs 167 */ 168 public synchronized void print(final Object value) throws IOException { 169 format.print(value, appendable, newRecord); 170 newRecord = false; 171 } 172 173 /** 174 * Prints a comment on a new line among the delimiter separated values. 175 * 176 * <p> 177 * Comments will always begin on a new line and occupy at least one full line. The character specified to start 178 * comments and a space will be inserted at the beginning of each new line in the comment. 179 * </p> 180 * 181 * <p> 182 * If comments are disabled in the current CSV format this method does nothing. 183 * </p> 184 * 185 * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()} 186 * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use 187 * line breaks as record separator.</p> 188 * 189 * @param comment 190 * the comment to output 191 * @throws IOException 192 * If an I/O error occurs 193 */ 194 public synchronized void printComment(final String comment) throws IOException { 195 if (comment == null || !format.isCommentMarkerSet()) { 196 return; 197 } 198 if (!newRecord) { 199 println(); 200 } 201 appendable.append(format.getCommentMarker().charValue()); 202 appendable.append(SP); 203 for (int i = 0; i < comment.length(); i++) { 204 final char c = comment.charAt(i); 205 switch (c) { 206 case CR: 207 if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) { 208 i++; 209 } 210 //$FALL-THROUGH$ break intentionally excluded. 211 case LF: 212 println(); 213 appendable.append(format.getCommentMarker().charValue()); 214 appendable.append(SP); 215 break; 216 default: 217 appendable.append(c); 218 break; 219 } 220 } 221 println(); 222 } 223 224 /** 225 * Prints headers for a result set based on its metadata. 226 * 227 * @param resultSet The result set to query for metadata. 228 * @throws IOException If an I/O error occurs. 229 * @throws SQLException If a database access error occurs or this method is called on a closed result set. 230 * @since 1.9.0 231 */ 232 public synchronized void printHeaders(final ResultSet resultSet) throws IOException, SQLException { 233 printRecord((Object[]) format.builder().setHeader(resultSet).build().getHeader()); 234 } 235 236 /** 237 * Outputs the record separator. 238 * 239 * @throws IOException 240 * If an I/O error occurs 241 */ 242 public synchronized void println() throws IOException { 243 format.println(appendable); 244 newRecord = true; 245 } 246 247 /** 248 * Prints the given values as a single record of delimiter separated values followed by the record separator. 249 * 250 * <p> 251 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 252 * separator to the output after printing the record, so there is no need to call {@link #println()}. 253 * </p> 254 * 255 * @param values 256 * values to output. 257 * @throws IOException 258 * If an I/O error occurs 259 */ 260 public synchronized void printRecord(final Iterable<?> values) throws IOException { 261 for (final Object value : values) { 262 print(value); 263 } 264 println(); 265 } 266 267 /** 268 * Prints the given values as a single record of delimiter separated values followed by the record separator. 269 * 270 * <p> 271 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 272 * separator to the output after printing the record, so there is no need to call {@link #println()}. 273 * </p> 274 * 275 * @param values 276 * values to output. 277 * @throws IOException 278 * If an I/O error occurs 279 */ 280 public void printRecord(final Object... values) throws IOException { 281 printRecord(Arrays.asList(values)); 282 } 283 284 /** 285 * Prints the given values as a single record of delimiter separated values followed by the record separator. 286 * 287 * <p> 288 * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record 289 * separator to the output after printing the record, so there is no need to call {@link #println()}. 290 * </p> 291 * 292 * @param values 293 * values to output. 294 * @throws IOException 295 * If an I/O error occurs 296 * @since 1.10.0 297 */ 298 public synchronized void printRecord(final Stream<?> values) throws IOException { 299 values.forEachOrdered(t -> { 300 try { 301 print(t); 302 } catch (final IOException e) { 303 throw IOUtils.rethrow(e); 304 } 305 }); 306 println(); 307 } 308 309 private void printRecordObject(final Object value) throws IOException { 310 if (value instanceof Object[]) { 311 this.printRecord((Object[]) value); 312 } else if (value instanceof Iterable) { 313 this.printRecord((Iterable<?>) value); 314 } else { 315 this.printRecord(value); 316 } 317 } 318 319 /** 320 * Prints all the objects in the given {@link Iterable} handling nested collections/arrays as records. 321 * 322 * <p> 323 * If the given Iterable only contains simple objects, this method will print a single record like 324 * {@link #printRecord(Iterable)}. If the given Iterable contains nested collections/arrays those nested elements 325 * will each be printed as records using {@link #printRecord(Object...)}. 326 * </p> 327 * 328 * <p> 329 * Given the following data structure: 330 * </p> 331 * 332 * <pre> 333 * <code> 334 * List<String[]> data = new ArrayList<>(); 335 * data.add(new String[]{ "A", "B", "C" }); 336 * data.add(new String[]{ "1", "2", "3" }); 337 * data.add(new String[]{ "A1", "B2", "C3" }); 338 * </code> 339 * </pre> 340 * 341 * <p> 342 * Calling this method will print: 343 * </p> 344 * 345 * <pre> 346 * <code> 347 * A, B, C 348 * 1, 2, 3 349 * A1, B2, C3 350 * </code> 351 * </pre> 352 * 353 * @param values 354 * the values to print. 355 * @throws IOException 356 * If an I/O error occurs 357 */ 358 public void printRecords(final Iterable<?> values) throws IOException { 359 for (final Object value : values) { 360 printRecordObject(value); 361 } 362 } 363 364 /** 365 * Prints all the objects in the given array handling nested collections/arrays as records. 366 * 367 * <p> 368 * If the given array only contains simple objects, this method will print a single record like 369 * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested 370 * elements will each be printed as records using {@link #printRecord(Object...)}. 371 * </p> 372 * 373 * <p> 374 * Given the following data structure: 375 * </p> 376 * 377 * <pre> 378 * <code> 379 * String[][] data = new String[3][] 380 * data[0] = String[]{ "A", "B", "C" }; 381 * data[1] = new String[]{ "1", "2", "3" }; 382 * data[2] = new String[]{ "A1", "B2", "C3" }; 383 * </code> 384 * </pre> 385 * 386 * <p> 387 * Calling this method will print: 388 * </p> 389 * 390 * <pre> 391 * <code> 392 * A, B, C 393 * 1, 2, 3 394 * A1, B2, C3 395 * </code> 396 * </pre> 397 * 398 * @param values 399 * the values to print. 400 * @throws IOException 401 * If an I/O error occurs 402 */ 403 public void printRecords(final Object... values) throws IOException { 404 printRecords(Arrays.asList(values)); 405 } 406 407 /** 408 * Prints all the objects in the given JDBC result set. 409 * 410 * @param resultSet 411 * result set the values to print. 412 * @throws IOException 413 * If an I/O error occurs 414 * @throws SQLException 415 * if a database access error occurs 416 */ 417 public void printRecords(final ResultSet resultSet) throws SQLException, IOException { 418 final int columnCount = resultSet.getMetaData().getColumnCount(); 419 while (resultSet.next()) { 420 for (int i = 1; i <= columnCount; i++) { 421 final Object object = resultSet.getObject(i); 422 // TODO Who manages the Clob? The JDBC driver or must we close it? Is it driver-dependent? 423 print(object instanceof Clob ? ((Clob) object).getCharacterStream() : object); 424 } 425 println(); 426 } 427 } 428 429 /** 430 * Prints all the objects with metadata in the given JDBC result set based on the header boolean. 431 * 432 * @param resultSet source of row data. 433 * @param printHeader whether to print headers. 434 * @throws IOException If an I/O error occurs 435 * @throws SQLException if a database access error occurs 436 * @since 1.9.0 437 */ 438 public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException { 439 if (printHeader) { 440 printHeaders(resultSet); 441 } 442 printRecords(resultSet); 443 } 444 445 /** 446 * Prints all the objects in the given {@link Stream} handling nested collections/arrays as records. 447 * 448 * <p> 449 * If the given Stream only contains simple objects, this method will print a single record like 450 * {@link #printRecord(Iterable)}. If the given Stream contains nested collections/arrays those nested elements 451 * will each be printed as records using {@link #printRecord(Object...)}. 452 * </p> 453 * 454 * <p> 455 * Given the following data structure: 456 * </p> 457 * 458 * <pre> 459 * <code> 460 * List<String[]> data = new ArrayList<>(); 461 * data.add(new String[]{ "A", "B", "C" }); 462 * data.add(new String[]{ "1", "2", "3" }); 463 * data.add(new String[]{ "A1", "B2", "C3" }); 464 * Stream<String[]> stream = data.stream(); 465 * </code> 466 * </pre> 467 * 468 * <p> 469 * Calling this method will print: 470 * </p> 471 * 472 * <pre> 473 * <code> 474 * A, B, C 475 * 1, 2, 3 476 * A1, B2, C3 477 * </code> 478 * </pre> 479 * 480 * @param values 481 * the values to print. 482 * @throws IOException 483 * If an I/O error occurs 484 * @since 1.10.0 485 */ 486 @SuppressWarnings("unused") // rethrow() throws IOException 487 public void printRecords(final Stream<?> values) throws IOException { 488 values.forEachOrdered(t -> { 489 try { 490 printRecordObject(t); 491 } catch (final IOException e) { 492 throw IOUtils.rethrow(e); 493 } 494 }); 495 } 496}