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;
032
033/**
034 * Prints values in a {@link CSVFormat CSV format}.
035 *
036 * <p>Values can be appended to the output by calling the {@link #print(Object)} method.
037 * Values are printed according to {@link String#valueOf(Object)}.
038 * To complete a record the {@link #println()} method has to be called.
039 * Comments can be appended by calling {@link #printComment(String)}.
040 * However a comment will only be written to the output if the {@link CSVFormat} supports comments.
041 * </p>
042 *
043 * <p>The printer also supports appending a complete record at once by calling {@link #printRecord(Object...)}
044 * or {@link #printRecord(Iterable)}.
045 * Furthermore {@link #printRecords(Object...)}, {@link #printRecords(Iterable)} and {@link #printRecords(ResultSet)}
046 * methods can be used to print several records at once.
047 * </p>
048 *
049 * <p>Example:</p>
050 *
051 * <pre>
052 * try (CSVPrinter printer = new CSVPrinter(new FileWriter("csv.txt"), CSVFormat.EXCEL)) {
053 *     printer.printRecord("id", "userName", "firstName", "lastName", "birthday");
054 *     printer.printRecord(1, "john73", "John", "Doe", LocalDate.of(1973, 9, 15));
055 *     printer.println();
056 *     printer.printRecord(2, "mary", "Mary", "Meyer", LocalDate.of(1985, 3, 29));
057 * } catch (IOException ex) {
058 *     ex.printStackTrace();
059 * }
060 * </pre>
061 *
062 * <p>This code will write the following to csv.txt:</p>
063 * <pre>
064 * id,userName,firstName,lastName,birthday
065 * 1,john73,John,Doe,1973-09-15
066 *
067 * 2,mary,Mary,Meyer,1985-03-29
068 * </pre>
069 */
070public final class CSVPrinter implements Flushable, Closeable {
071
072    /** The place that the values get written. */
073    private final Appendable appendable;
074    private final CSVFormat format;
075
076    /** True if we just began a new record. */
077    private boolean newRecord = true;
078
079    /**
080     * Creates a printer that will print values to the given stream following the CSVFormat.
081     * <p>
082     * Currently, only a pure encapsulation format or a pure escaping format is supported. Hybrid formats (encapsulation
083     * and escaping with a different character) are not supported.
084     * </p>
085     *
086     * @param appendable
087     *            stream to which to print. Must not be null.
088     * @param format
089     *            the CSV format. Must not be null.
090     * @throws IOException
091     *             thrown if the optional header cannot be printed.
092     * @throws IllegalArgumentException
093     *             thrown if the parameters of the format are inconsistent or if either out or format are null.
094     */
095    public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException {
096        Objects.requireNonNull(appendable, "appendable");
097        Objects.requireNonNull(format, "format");
098
099        this.appendable = appendable;
100        this.format = format.copy();
101        // TODO: Is it a good idea to do this here instead of on the first call to a print method?
102        // It seems a pain to have to track whether the header has already been printed or not.
103        if (format.getHeaderComments() != null) {
104            for (final String line : format.getHeaderComments()) {
105                this.printComment(line);
106            }
107        }
108        if (format.getHeader() != null && !format.getSkipHeaderRecord()) {
109            this.printRecord((Object[]) format.getHeader());
110        }
111    }
112
113    @Override
114    public void close() throws IOException {
115        close(false);
116    }
117
118    /**
119     * Closes the underlying stream with an optional flush first.
120     * @param flush whether to flush before the actual close.
121     *
122     * @throws IOException
123     *             If an I/O error occurs
124     * @since 1.6
125     */
126    public void close(final boolean flush) throws IOException {
127        if (flush || format.getAutoFlush()) {
128            flush();
129        }
130        if (appendable instanceof Closeable) {
131            ((Closeable) appendable).close();
132        }
133    }
134
135    /**
136     * Flushes the underlying stream.
137     *
138     * @throws IOException
139     *             If an I/O error occurs
140     */
141    @Override
142    public void flush() throws IOException {
143        if (appendable instanceof Flushable) {
144            ((Flushable) appendable).flush();
145        }
146    }
147
148    /**
149     * Gets the target Appendable.
150     *
151     * @return the target Appendable.
152     */
153    public Appendable getOut() {
154        return this.appendable;
155    }
156
157    /**
158     * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed.
159     *
160     * @param value
161     *            value to be output.
162     * @throws IOException
163     *             If an I/O error occurs
164     */
165    public void print(final Object value) throws IOException {
166        format.print(value, appendable, newRecord);
167        newRecord = false;
168    }
169
170    /**
171     * Prints a comment on a new line among the delimiter separated values.
172     *
173     * <p>
174     * Comments will always begin on a new line and occupy at least one full line. The character specified to start
175     * comments and a space will be inserted at the beginning of each new line in the comment.
176     * </p>
177     *
178     * <p>
179     * If comments are disabled in the current CSV format this method does nothing.
180     * </p>
181     *
182     * <p>This method detects line breaks inside the comment string and inserts {@link CSVFormat#getRecordSeparator()}
183     * to start a new line of the comment. Note that this might produce unexpected results for formats that do not use
184     * line breaks as record separator.</p>
185     *
186     * @param comment
187     *            the comment to output
188     * @throws IOException
189     *             If an I/O error occurs
190     */
191    public void printComment(final String comment) throws IOException {
192        if (comment == null || !format.isCommentMarkerSet()) {
193            return;
194        }
195        if (!newRecord) {
196            println();
197        }
198        appendable.append(format.getCommentMarker().charValue());
199        appendable.append(SP);
200        for (int i = 0; i < comment.length(); i++) {
201            final char c = comment.charAt(i);
202            switch (c) {
203            case CR:
204                if (i + 1 < comment.length() && comment.charAt(i + 1) == LF) {
205                    i++;
206                }
207                //$FALL-THROUGH$ break intentionally excluded.
208            case LF:
209                println();
210                appendable.append(format.getCommentMarker().charValue());
211                appendable.append(SP);
212                break;
213            default:
214                appendable.append(c);
215                break;
216            }
217        }
218        println();
219    }
220
221    /**
222     * Prints headers for a result set based on its metadata.
223     *
224     * @param resultSet The result set to query for metadata.
225     * @throws IOException If an I/O error occurs.
226     * @throws SQLException If a database access error occurs or this method is called on a closed result set.
227     * @since 1.9.0
228     */
229    public void printHeaders(final ResultSet resultSet) throws IOException, SQLException {
230        printRecord((Object[]) format.builder().setHeader(resultSet).build().getHeader());
231    }
232
233    /**
234     * Outputs the record separator.
235     *
236     * @throws IOException
237     *             If an I/O error occurs
238     */
239    public void println() throws IOException {
240        format.println(appendable);
241        newRecord = true;
242    }
243
244    /**
245     * Prints the given values a single record of delimiter separated values followed by the record separator.
246     *
247     * <p>
248     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
249     * separator to the output after printing the record, so there is no need to call {@link #println()}.
250     * </p>
251     *
252     * @param values
253     *            values to output.
254     * @throws IOException
255     *             If an I/O error occurs
256     */
257    public void printRecord(final Iterable<?> values) throws IOException {
258        for (final Object value : values) {
259            print(value);
260        }
261        println();
262    }
263
264    /**
265     * Prints the given values a single record of delimiter separated values followed by the record separator.
266     *
267     * <p>
268     * The values will be quoted if needed. Quotes and newLine characters will be escaped. This method adds the record
269     * separator to the output after printing the record, so there is no need to call {@link #println()}.
270     * </p>
271     *
272     * @param values
273     *            values to output.
274     * @throws IOException
275     *             If an I/O error occurs
276     */
277    public void printRecord(final Object... values) throws IOException {
278        printRecord(Arrays.asList(values));
279    }
280
281    /**
282     * Prints all the objects in the given collection handling nested collections/arrays as records.
283     *
284     * <p>
285     * If the given collection only contains simple objects, this method will print a single record like
286     * {@link #printRecord(Iterable)}. If the given collections contains nested collections/arrays those nested elements
287     * will each be printed as records using {@link #printRecord(Object...)}.
288     * </p>
289     *
290     * <p>
291     * Given the following data structure:
292     * </p>
293     *
294     * <pre>
295     * <code>
296     * List&lt;String[]&gt; data = ...
297     * data.add(new String[]{ "A", "B", "C" });
298     * data.add(new String[]{ "1", "2", "3" });
299     * data.add(new String[]{ "A1", "B2", "C3" });
300     * </code>
301     * </pre>
302     *
303     * <p>
304     * Calling this method will print:
305     * </p>
306     *
307     * <pre>
308     * <code>
309     * A, B, C
310     * 1, 2, 3
311     * A1, B2, C3
312     * </code>
313     * </pre>
314     *
315     * @param values
316     *            the values to print.
317     * @throws IOException
318     *             If an I/O error occurs
319     */
320    public void printRecords(final Iterable<?> values) throws IOException {
321        for (final Object value : values) {
322            if (value instanceof Object[]) {
323                this.printRecord((Object[]) value);
324            } else if (value instanceof Iterable) {
325                this.printRecord((Iterable<?>) value);
326            } else {
327                this.printRecord(value);
328            }
329        }
330    }
331
332    /**
333     * Prints all the objects in the given array handling nested collections/arrays as records.
334     *
335     * <p>
336     * If the given array only contains simple objects, this method will print a single record like
337     * {@link #printRecord(Object...)}. If the given collections contains nested collections/arrays those nested
338     * elements will each be printed as records using {@link #printRecord(Object...)}.
339     * </p>
340     *
341     * <p>
342     * Given the following data structure:
343     * </p>
344     *
345     * <pre>
346     * <code>
347     * String[][] data = new String[3][]
348     * data[0] = String[]{ "A", "B", "C" };
349     * data[1] = new String[]{ "1", "2", "3" };
350     * data[2] = new String[]{ "A1", "B2", "C3" };
351     * </code>
352     * </pre>
353     *
354     * <p>
355     * Calling this method will print:
356     * </p>
357     *
358     * <pre>
359     * <code>
360     * A, B, C
361     * 1, 2, 3
362     * A1, B2, C3
363     * </code>
364     * </pre>
365     *
366     * @param values
367     *            the values to print.
368     * @throws IOException
369     *             If an I/O error occurs
370     */
371    public void printRecords(final Object... values) throws IOException {
372        printRecords(Arrays.asList(values));
373    }
374
375    /**
376     * Prints all the objects in the given JDBC result set.
377     *
378     * @param resultSet
379     *            result set the values to print.
380     * @throws IOException
381     *             If an I/O error occurs
382     * @throws SQLException
383     *             if a database access error occurs
384     */
385    public void printRecords(final ResultSet resultSet) throws SQLException, IOException {
386        final int columnCount = resultSet.getMetaData().getColumnCount();
387        while (resultSet.next()) {
388            for (int i = 1; i <= columnCount; i++) {
389                final Object object = resultSet.getObject(i);
390                // TODO Who manages the Clob? The JDBC driver or must we close it? Is it driver-dependent?
391                print(object instanceof Clob ? ((Clob) object).getCharacterStream() : object);
392            }
393            println();
394        }
395    }
396
397    /**
398     * Prints all the objects with metadata in the given JDBC result set based on the header boolean.
399     *
400     * @param resultSet source of row data.
401     * @param printHeader whether to print headers.
402     * @throws IOException If an I/O error occurs
403     * @throws SQLException if a database access error occurs
404     * @since 1.9.0
405     */
406    public void printRecords(final ResultSet resultSet, final boolean printHeader) throws SQLException, IOException {
407        if (printHeader) {
408            printHeaders(resultSet);
409        }
410        printRecords(resultSet);
411    }
412}