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 */
017package org.apache.wicket.extensions.markup.html.repeater.data.table.export;
018
019import java.io.Closeable;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.io.OutputStreamWriter;
023import java.io.Writer;
024import java.nio.charset.Charset;
025import java.util.Iterator;
026import java.util.List;
027
028import org.apache.wicket.Application;
029import org.apache.wicket.IConverterLocator;
030import org.apache.wicket.Session;
031import org.apache.wicket.markup.repeater.data.IDataProvider;
032import org.apache.wicket.model.IModel;
033import org.apache.wicket.model.Model;
034import org.apache.wicket.util.convert.IConverter;
035import org.apache.wicket.util.lang.Args;
036
037/**
038 * An {@link IDataExporter} that exports data to a CSV file. This class allows for customization of the exact CSV format, including
039 * setting the delimiter, the text quoting character and the character set.
040 * <p>
041 * This class will export CSV files in a format consistent with RFC4180 by default.
042 *
043 * @author Jesse Long
044 */
045public class CSVDataExporter extends AbstractDataExporter
046{
047        private char delimiter = ',';
048
049        private String characterSet = "utf-8";
050
051        private char quoteCharacter = '"';
052
053        private boolean exportHeadersEnabled = true;
054
055        /**
056         * Creates a new instance.
057         */
058        public CSVDataExporter()
059        {
060                super(Model.of("CSV"), "text/csv", "csv");
061        }
062
063        /**
064         * Sets the delimiter to be used to separate fields. The default delimiter is a colon.
065         *
066         * @param delimiter
067         *      The delimiter to be used to separate fields.
068         * @return {@code this}, for chaining.
069         */
070        public CSVDataExporter setDelimiter(char delimiter)
071        {
072                this.delimiter = delimiter;
073                return this;
074        }
075
076        /**
077         * Returns the delimiter to be used for separating fields.
078         *
079         * @return the delimiter to be used for separating fields.
080         */
081        public char getDelimiter()
082        {
083                return delimiter;
084        }
085
086        /**
087         * Returns the character set encoding to be used when exporting data.
088         *
089         * @return the character set encoding to be used when exporting data.
090         */
091        public String getCharacterSet()
092        {
093                return characterSet;
094        }
095
096        /**
097         * Sets the character set encoding to be used when exporting data. This defaults to UTF-8.
098         *
099         * @param characterSet
100         *      The character set encoding to be used when exporting data.
101         * @return {@code this}, for chaining.
102         */
103        public CSVDataExporter setCharacterSet(String characterSet)
104        {
105                this.characterSet = Args.notNull(characterSet, "characterSer");
106                return this;
107        }
108
109        /**
110         * Returns the character to be used for quoting fields.
111         *
112         * @return the character to be used for quoting fields.
113         */
114        public char getQuoteCharacter()
115        {
116                return quoteCharacter;
117        }
118
119        /**
120         * Sets the character to be used to quote fields. This defaults to double quotes,
121         *
122         * @param quoteCharacter
123         *      The character to be used to quote fields.
124         * @return {@code this}, for chaining.
125         */
126        public CSVDataExporter setQuoteCharacter(char quoteCharacter)
127        {
128                this.quoteCharacter = quoteCharacter;
129                return this;
130        }
131
132        /**
133         * Returns the content type of the exported data. For CSV, this is normally
134         * "text/csv". This methods adds the character set and header values, in accordance with
135         * RFC4180.
136         *
137         * @return  the content type of the exported data.
138         */
139        @Override
140        public String getContentType()
141        {
142                return super.getContentType() + "; charset=" + characterSet + "; header=" + ((exportHeadersEnabled) ? "present" : "absent");
143        }
144
145        /**
146         * Turns on or off export headers functionality. If this is set to {@code true}, then the first
147         * line of the export will contain the column headers. This defaults to {@code true}.
148         *
149         * @param exportHeadersEnabled
150         *      A boolean indicating whether or not headers should be exported.
151         * @return {@code this}, for chaining.
152         */
153        public CSVDataExporter setExportHeadersEnabled(boolean exportHeadersEnabled)
154        {
155                this.exportHeadersEnabled = exportHeadersEnabled;
156                return this;
157        }
158
159        /**
160         * Indicates if header exporting is enabled. Defaults to {@code true}.
161         *
162         * @return a boolean indicating if header exporting is enabled.
163         */
164        public boolean isExportHeadersEnabled()
165        {
166                return exportHeadersEnabled;
167        }
168
169        /**
170         * Quotes a value for export to CSV. According to RFC4180, this should just duplicate all occurrences
171         * of the quote character and wrap the result in the quote character.
172         *
173         * @param value
174         *      The value to be quoted.
175         * @return a quoted copy of the value.
176         */
177        protected String quoteValue(String value)
178        {
179                return quoteCharacter + value.replace("" + quoteCharacter, "" + quoteCharacter + quoteCharacter) + quoteCharacter;
180        }
181
182        @Override
183        public <T> void exportData(IDataProvider<T> dataProvider, List<IExportableColumn<T, ?>> columns, OutputStream outputStream)
184                throws IOException
185        {
186                
187                try (Grid grid = new Grid(new OutputStreamWriter(outputStream, Charset.forName(characterSet))))
188                {
189                        writeHeaders(columns, grid);
190                        writeData(dataProvider, columns, grid);
191                }
192        }
193        
194        private <T> void writeHeaders(List<IExportableColumn<T, ?>> columns, Grid grid) throws IOException
195        {
196                if (isExportHeadersEnabled())
197                {
198                        for (IExportableColumn<T, ?> col : columns)
199                        {
200                                IModel<String> displayModel = col.getDisplayModel();
201                                String display = wrapModel(displayModel).getObject();
202                                grid.cell(quoteValue(display));
203                        }
204                        grid.row();
205                }
206        }
207
208        @SuppressWarnings({ "rawtypes", "unchecked" })
209        private <T> void writeData(IDataProvider<T> dataProvider, List<IExportableColumn<T, ?>> columns, Grid grid) throws IOException
210        {
211                long numberOfRows = dataProvider.size();
212                Iterator<? extends T> rowIterator = dataProvider.iterator(0, numberOfRows);
213                while (rowIterator.hasNext())
214                {
215                        T row = rowIterator.next();
216
217                        for (IExportableColumn<T, ?> col : columns)
218                        {
219                                IModel<?> dataModel = col.getDataModel(dataProvider.model(row));
220                                
221                                Object value = wrapModel(dataModel).getObject();
222                                if (value != null)
223                                {
224                                        Class<?> c = value.getClass();
225
226                                        String s;
227
228                                        IConverter converter = getConverterLocator().getConverter(c);
229
230                                        if (converter == null)
231                                        {
232                                                s = value.toString();
233                                        }
234                                        else
235                                        {
236                                                s = converter.convertToString(value, Session.get().getLocale());
237                                        }
238
239                                        grid.cell(quoteValue(s));
240                                }
241                        }
242                        grid.row();
243                }
244        }
245
246        /**
247         * Get the locator of converters.
248         *
249         * @return locator
250         */
251        protected IConverterLocator getConverterLocator() {
252                return Application.get().getConverterLocator();
253        }
254
255        /**
256         * Wrap the given model-
257         * 
258         * @param model
259         * @return
260         */
261        protected <T> IModel<T> wrapModel(IModel<T> model)
262        {
263                return model;
264        }
265        
266        private class Grid implements Closeable{
267
268                private Writer writer;
269
270                boolean first = true;
271                
272                public Grid(Writer writer)
273                {
274                        this.writer = writer;
275                }
276
277                public void cell(String value) throws IOException {
278                        if (first)
279                        {
280                                first = false;
281                        }
282                        else
283                        {
284                                writer.write(delimiter);
285                        }
286                        
287                        writer.write(value);
288                }
289                
290                public void row() throws IOException
291                {
292                        writer.write("\r\n");
293                        first = true;
294                }
295
296                @Override
297                public void close() throws IOException
298                {
299                        writer.close();
300                }
301        }
302}