/* ================================================================== * SimpleCsvHttpMessageConverter.java - Dec 3, 2013 2:50:27 PM * * Copyright 2007-2013 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 * ================================================================== */ package net.solarnetwork.web.support; import java.beans.PropertyEditor; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; 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 net.solarnetwork.util.ClassUtils; import net.solarnetwork.util.PropertySerializerRegistrar; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.supercsv.io.CsvMapWriter; import org.supercsv.io.ICsvMapWriter; import org.supercsv.prefs.CsvPreference; /** * {@link HttpMessageConverter} that marshals objects into CSV documents. * * @author matt * @version 1.1 */ public class SimpleCsvHttpMessageConverter extends AbstractHttpMessageConverter<Object> { /** * The default value for the <code>javaBeanIgnoreProperties</code> property. */ public static final String[] DEFAULT_JAVA_BEAN_IGNORE_PROPERTIES = new String[] { "class", }; /** * The default value for the <code>javaBeanTreatAsStringValues</code> * property. */ public static final Class<?>[] DEFAULT_JAVA_BEAN_STRING_VALUES = new Class<?>[] { Class.class, }; private PropertySerializerRegistrar propertySerializerRegistrar = null; private Set<String> javaBeanIgnoreProperties = new LinkedHashSet<String>( Arrays.asList(DEFAULT_JAVA_BEAN_IGNORE_PROPERTIES)); private Set<Class<?>> javaBeanTreatAsStringValues = new LinkedHashSet<Class<?>>( Arrays.asList(DEFAULT_JAVA_BEAN_STRING_VALUES)); private boolean includeHeader = true; /** * Default constructor. */ public SimpleCsvHttpMessageConverter() { super(new MediaType("text", "csv", Charset.forName("UTF-8"))); } @Override protected boolean supports(Class<?> clazz) { return true; } @Override protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException("Reading CSV is not supported."); } @Override protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { Iterable<?> rows = null; if ( t instanceof Iterable ) { rows = (Iterable<?>) t; } else { // see if object has an Iterable property on it Map<String, Object> props = ClassUtils.getBeanProperties(t, javaBeanIgnoreProperties); for ( Object o : props.values() ) { if ( o instanceof Iterable ) { rows = (Iterable<?>) o; break; } } if ( rows == null ) { List<Object> tmpList = new ArrayList<Object>(1); tmpList.add(t); 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, null); final String[] fields = fieldList.toArray(new String[fieldList.size()]); if ( fields == null || fields.length < 1 ) { // could happen with empty Map, for example return; } final ICsvMapWriter writer = new CsvMapWriter(new OutputStreamWriter(outputMessage.getBody(), "UTF-8"), CsvPreference.EXCEL_PREFERENCE); try { // output header if ( includeHeader ) { writer.writeHeader(fields); } // output first row writeCSV(writer, fields, row); // output remainder rows while ( rowIterator.hasNext() ) { row = rowIterator.next(); writeCSV(writer, fields, row); } } finally { if ( writer != null ) { try { writer.flush(); writer.close(); } catch ( IOException e ) { // ignore these } } } } 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 ( propertySerializerRegistrar != null ) { // try whole-bean serialization first Object o = propertySerializerRegistrar .serializeProperty("row", row.getClass(), row, row); if ( o != row ) { if ( o != null ) { result = getCSVFields(o, fieldOrder); return result; } } } Map<String, Object> props = ClassUtils .getBeanProperties(row, javaBeanIgnoreProperties, true); result = getCSVFields(props, fieldOrder); } return result; } // this method exists so we don't have to add @SuppressWarnings to other (real) methods @SuppressWarnings("unchecked") private <T> T cast(Object o) { return (T) o; } 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 ( propertySerializerRegistrar != null ) { // try whole-bean serialization first row = propertySerializerRegistrar.serializeProperty("row", row.getClass(), row, row); if ( row == null ) { return; } } if ( row instanceof Map ) { Map<String, ?> rowMap = cast(row); for ( Map.Entry<String, ?> me : rowMap.entrySet() ) { Object val = getRowPropertyValue(row, me.getKey(), me.getValue(), null); if ( val != null ) { map.put(me.getKey(), val); } } } else { BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(row); for ( String name : fields ) { Object val = wrapper.getPropertyValue(name); val = getRowPropertyValue(row, name, val, wrapper); if ( val != null ) { map.put(name, val); } } } writer.write(map, fields); } } private Object getRowPropertyValue(Object row, String name, Object val, BeanWrapper wrapper) { if ( val != null ) { if ( getPropertySerializerRegistrar() != null ) { val = getPropertySerializerRegistrar().serializeProperty(name, val.getClass(), row, val); } else if ( wrapper != null ) { // 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<?> || javaBeanTreatAsStringValues != null && javaBeanTreatAsStringValues.contains(val.getClass()) ) { val = val.toString(); } } return val; } public PropertySerializerRegistrar getPropertySerializerRegistrar() { return propertySerializerRegistrar; } public void setPropertySerializerRegistrar(PropertySerializerRegistrar propertySerializerRegistrar) { this.propertySerializerRegistrar = propertySerializerRegistrar; } public Set<String> getJavaBeanIgnoreProperties() { return javaBeanIgnoreProperties; } public void setJavaBeanIgnoreProperties(Set<String> javaBeanIgnoreProperties) { this.javaBeanIgnoreProperties = javaBeanIgnoreProperties; } public Set<Class<?>> getJavaBeanTreatAsStringValues() { return javaBeanTreatAsStringValues; } public void setJavaBeanTreatAsStringValues(Set<Class<?>> javaBeanTreatAsStringValues) { this.javaBeanTreatAsStringValues = javaBeanTreatAsStringValues; } public boolean isIncludeHeader() { return includeHeader; } public void setIncludeHeader(boolean includeHeader) { this.includeHeader = includeHeader; } }