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}