001    
002    /*
003     * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: CSVOutput.java 308 2012-03-10 22:26:06Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.string;
009    
010    import au.com.bytecode.opencsv.CSVWriter;
011    
012    import java.io.BufferedWriter;
013    import java.io.IOException;
014    import java.io.Writer;
015    import java.util.Arrays;
016    import java.util.Date;
017    import java.util.HashSet;
018    import java.util.LinkedHashSet;
019    import java.util.Map;
020    
021    /**
022     * CSV file output stream that ensures values are matched to the correct columns.
023     * This class requires the <a href="http://opencsv.sourceforge.net/">OpenCSV</a> library.
024     *
025     * @see <a href="http://opencsv.sourceforge.net/">OpenCSV</a>
026     */
027    public class CSVOutput {
028    
029        private final CSVWriter writer;
030        private final String[] columns;
031    
032        /**
033         * Constructor.
034         *
035         * @param writer destination for the CSV output
036         * @param columns CSV columns names in their desired order
037         * @throws IllegalArgumentException if {@code writer} is null
038         * @throws IllegalArgumentException if {@code columns} is null
039         * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
040         */
041        public CSVOutput(Writer writer, String... columns) {
042            this(writer, Arrays.asList(columns));
043        }
044    
045        /**
046         * Constructor.
047         *
048         * @param writer destination for the CSV output
049         * @param columns CSV columns names, iterated in their desired order
050         * @throws IllegalArgumentException if {@code writer} is null
051         * @throws IllegalArgumentException if {@code columns} is null
052         * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
053         */
054        public CSVOutput(Writer writer, Iterable<String> columns) {
055            this(new CSVWriter(new BufferedWriter(writer)), columns);
056            if (writer == null)
057                throw new IllegalArgumentException("null writer");
058        }
059    
060        /**
061         * Constructor.
062         *
063         * @param writer CSV output object
064         * @param columns CSV columns names, iterated in their desired order
065         * @throws IllegalArgumentException if {@code writer} is null
066         * @throws IllegalArgumentException if {@code columns} is null
067         * @throws IllegalArgumentException if {@code columns} contains a duplicate column name
068         */
069        public CSVOutput(CSVWriter writer, Iterable<String> columns) {
070            if (writer == null)
071                throw new IllegalArgumentException("null writer");
072            if (columns == null)
073                throw new IllegalArgumentException("null columns");
074            this.writer = writer;
075            LinkedHashSet<String> columnSet = new LinkedHashSet<String>();
076            for (String column : columns) {
077                if (!columnSet.add(column))
078                    throw new IllegalArgumentException("duplicate column name `" + column + "'");
079            }
080            this.columns = columnSet.toArray(new String[columnSet.size()]);
081    
082            // Output header
083            this.writer.writeNext(this.columns);
084        }
085    
086        /**
087         * Output a CSV row.
088         *
089         * @param row mapping from column name to value; missing or values are treated as null
090         * @throws IllegalArgumentException if {@code row} contains an unknown column name
091         */
092        public void writeRow(Map<String, ?> row) {
093    
094            // Sanity check column names
095            HashSet<String> unknowns = new HashSet<String>(row.keySet());
096            unknowns.removeAll(Arrays.asList(this.columns));
097            if (!unknowns.isEmpty())
098                throw new IllegalArgumentException("row contains unknown column(s): " + unknowns);
099    
100            // Format columns
101            String[] values = new String[this.columns.length];
102            for (int i = 0; i < this.columns.length; i++)
103                values[i] = this.formatObject(this.columns[i], row.get(this.columns[i]));
104    
105            // Output row
106            this.writer.writeNext(values);
107        }
108    
109        /**
110         * Flush output.
111         */
112        public void flush() throws IOException {
113            this.writer.flush();
114        }
115    
116        /**
117         * Close this instance and the underlying output.
118         */
119        public void close() throws IOException {
120            this.writer.close();
121        }
122    
123        /**
124         * Format a CSV column value.
125         *
126         * <p>
127         * The implementation in {@link CSVOutput} applies the following logic:
128         * <ul>
129         * <li>{@code null} values are output as the empty string</li>
130         * <li>{@link Boolean} values are output as {@code 1} or {@code 0}</li>
131         * <li>{@link Date} values are output by delegating to {@link #formatDate formatDate()}</li>
132         * <li>All other objects are output using {@link String#valueOf}</li>
133         * </ul>
134         * </p>
135         *
136         * <p>
137         * Subclasses should override as needed.
138         * </p>
139         *
140         * @param columnName name of the column
141         * @param value column value; will be null if no value was present in the {@link Map} parameter to {@link #writeRow}
142         */
143        protected String formatObject(String columnName, Object value) {
144            if (value == null)
145                return "";
146            if (value instanceof Boolean)
147                return ((Boolean)value).booleanValue() ? "1" : "0";
148            if (value instanceof Date)
149                return this.formatDate(columnName, (Date)value);
150            return String.valueOf(value);
151        }
152    
153        /**
154         * Format a {@link Date} value.
155         *
156         * <p>
157         * The implementation in {@link CSVOutput} delegates to {@link DateEncoder#encode DateEncoder.encode()}.
158         * </p>
159         *
160         * <p>
161         * Subclasses should override as needed.
162         * </p>
163         *
164         * @param columnName name of the column
165         * @param date column value
166         * @throws IllegalArgumentException if {@code date} is null
167         */
168        protected String formatDate(String columnName, Date date) {
169            if (date == null)
170                throw new IllegalArgumentException("null date");
171            return DateEncoder.encode(date);
172        }
173    }
174