/* ==================================================================
* SimpleCsvView.java - Feb 11, 2012 3:03:32 PM
*
* Copyright 2007-2012 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
* $Id$
* ==================================================================
*/
package net.solarnetwork.web.support;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.solarnetwork.util.SerializeIgnore;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.supercsv.io.CsvMapWriter;
import org.supercsv.io.ICsvMapWriter;
import org.supercsv.prefs.CsvPreference;
/**
* Spring {@link org.springframework.web.servlet.View} for turning objects into
* CSV through JavaBean introspection.
*
* <p>
* The character encoding of the output must be specified in the
* {@link #setContentType(String)} (e.g. {@literal text/csv;charset=UTF-8}).
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl>
* <dt>dataModelKey</dt>
* <dd>If not <em>null</em>, then use this model key as the data object to
* render as CSV. Otherwise, export just the first available key's associated
* object. Defaults to {@link #DEFAULT_DATA_MODEL_KEY}.</dd>
*
* <dt>fieldOrderKey</dt>
* <dd>If not <em>null</em>, then use this model key as an ordered Collection of
* exported field names, such that the CSV columns will be exported in the
* specified order. If not specified, then for Map objects the output order will
* be determined by the natural iteration order of the Map keys, and for
* JavaBean objects the bean properties will be exported in case-insensitive
* alphabetical order. Defaults to {@link #DEFAULT_FIELD_ORDER_KEY}.</dd>
* </dl>
*
*
* @author matt
* @version $Revision$
*/
public class SimpleCsvView extends AbstractView {
/** Default content type. */
public static final String DEFAULT_CSV_CONTENT_TYPE = "text/csv;charset=UTF-8";
/** The default value for the {@code dataModelKey} property. */
public static final String DEFAULT_DATA_MODEL_KEY = "data";
/** The default value for the {@code fieldOrderKey} property. */
public static final String DEFAULT_FIELD_ORDER_KEY = "fieldOrder";
private String dataModelKey = DEFAULT_DATA_MODEL_KEY;
private String fieldOrderKey = DEFAULT_FIELD_ORDER_KEY;
/**
* Default constructor.
*/
public SimpleCsvView() {
super();
setContentType(DEFAULT_CSV_CONTENT_TYPE);
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if ( model.isEmpty() ) {
return;
}
final String charset = getResponseCharacterEncoding();
response.setCharacterEncoding(charset);
response.setContentType(getContentType());
final Object data = (dataModelKey != null && model.containsKey(dataModelKey) ? model
.get(dataModelKey) : model);
if ( data == null ) {
return;
}
@SuppressWarnings("unchecked")
final Collection<String> fieldOrder = (fieldOrderKey != null
&& model.get(fieldOrderKey) instanceof Collection ? (Collection<String>) model
.get(fieldOrderKey) : null);
Iterable<?> rows = null;
if ( data instanceof Iterable ) {
rows = (Iterable<?>) data;
} else {
List<Object> tmpList = new ArrayList<Object>(1);
tmpList.add(data);
rows = tmpList;
}
Object row = null;
Iterator<?> rowIterator = rows.iterator();
if ( !rowIterator.hasNext() ) {
return;
}
// get first row, to use for fields
row = rowIterator.next();
if ( row == null ) {
return;
}
final List<String> fieldList = getCSVFields(row, fieldOrder);
final String[] fields = fieldList.toArray(new String[fieldList.size()]);
final ICsvMapWriter writer = new CsvMapWriter(response.getWriter(),
CsvPreference.EXCEL_PREFERENCE);
try {
// output header
if ( true ) { // TODO make configurable property
Map<String, String> headerMap = new HashMap<String, String>(fields.length);
for ( String field : fields ) {
headerMap.put(field, field);
}
writeCSV(writer, fields, headerMap);
}
// output first row
writeCSV(writer, fields, row);
// output remainder rows
while ( rowIterator.hasNext() ) {
row = rowIterator.next();
writeCSV(writer, fields, row);
}
} finally {
writer.flush();
writer.close();
}
}
private List<String> getCSVFields(Object row, final Collection<String> fieldOrder) {
assert row != null;
List<String> result = new ArrayList<String>();
if ( row instanceof Map ) {
Map<?, ?> map = (Map<?, ?>) row;
if ( fieldOrder != null ) {
for ( String key : fieldOrder ) {
result.add(key);
}
} else {
for ( Object key : map.keySet() ) {
result.add(key.toString());
}
}
} else {
// use bean properties
if ( getPropertySerializerRegistrar() != null ) {
// try whole-bean serialization first
Object o = getPropertySerializerRegistrar().serializeProperty("row", row.getClass(),
row, row);
if ( o != row ) {
if ( o != null ) {
result = getCSVFields(o, fieldOrder);
return result;
}
}
}
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(row);
PropertyDescriptor[] props = wrapper.getPropertyDescriptors();
Set<String> resultSet = new LinkedHashSet<String>();
for ( PropertyDescriptor prop : props ) {
String name = prop.getName();
if ( getJavaBeanIgnoreProperties() != null
&& getJavaBeanIgnoreProperties().contains(name) ) {
continue;
}
if ( wrapper.isReadableProperty(name) ) {
// test for SerializeIgnore
Method getter = prop.getReadMethod();
if ( getter != null && getter.isAnnotationPresent(SerializeIgnore.class) ) {
continue;
}
resultSet.add(name);
}
}
if ( fieldOrder != null && fieldOrder.size() > 0 ) {
for ( String key : fieldOrder ) {
if ( resultSet.contains(key) ) {
result.add(key);
}
}
} else {
result.addAll(resultSet);
}
}
return result;
}
private void writeCSV(ICsvMapWriter writer, String[] fields, Object row) throws IOException {
if ( row instanceof Map ) {
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) row;
writer.write(map, fields);
} else if ( row != null ) {
Map<String, Object> map = new HashMap<String, Object>(fields.length);
// use bean properties
if ( getPropertySerializerRegistrar() != null ) {
// try whole-bean serialization first
row = getPropertySerializerRegistrar()
.serializeProperty("row", row.getClass(), row, row);
if ( row == null ) {
return;
}
}
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(row);
for ( String name : fields ) {
Object val = wrapper.getPropertyValue(name);
if ( val != null ) {
if ( getPropertySerializerRegistrar() != null ) {
val = getPropertySerializerRegistrar().serializeProperty(name, val.getClass(),
row, val);
} else {
// Spring does not apply PropertyEditors on read methods, so manually handle
PropertyEditor editor = wrapper.findCustomEditor(null, name);
if ( editor != null ) {
editor.setValue(val);
val = editor.getAsText();
}
}
if ( val instanceof Enum<?> || getJavaBeanTreatAsStringValues() != null
&& getJavaBeanTreatAsStringValues().contains(val.getClass()) ) {
val = val.toString();
}
if ( val != null ) {
map.put(name, val);
}
}
}
writer.write(map, fields);
}
}
public String getDataModelKey() {
return dataModelKey;
}
public void setDataModelKey(String dataModelKey) {
this.dataModelKey = dataModelKey;
}
public String getFieldOrderKey() {
return fieldOrderKey;
}
public void setFieldOrderKey(String fieldOrderKey) {
this.fieldOrderKey = fieldOrderKey;
}
}