package org.simpleflatmapper.csv; import org.simpleflatmapper.converter.Converter; import org.simpleflatmapper.converter.ConverterService; import org.simpleflatmapper.converter.ToStringConverter; import org.simpleflatmapper.csv.impl.writer.CsvCellWriter; import org.simpleflatmapper.csv.mapper.FieldMapperToAppendableFactory; import org.simpleflatmapper.map.FieldMapper; import org.simpleflatmapper.map.Mapper; import org.simpleflatmapper.map.MapperBuilderErrorHandler; import org.simpleflatmapper.map.MappingContext; import org.simpleflatmapper.map.PropertyWithGetter; import org.simpleflatmapper.map.context.KeySourceGetter; import org.simpleflatmapper.map.context.MappingContextFactoryBuilder; import org.simpleflatmapper.map.mapper.PropertyMapping; import org.simpleflatmapper.map.property.FormatProperty; import org.simpleflatmapper.map.property.FieldMapperColumnDefinition; import org.simpleflatmapper.map.MapperConfig; import org.simpleflatmapper.map.mapper.ContextualMapper; import org.simpleflatmapper.reflect.ReflectionService; import org.simpleflatmapper.reflect.meta.ClassMeta; import org.simpleflatmapper.reflect.meta.PropertyMeta; import org.simpleflatmapper.util.Consumer; import org.simpleflatmapper.util.ErrorHelper; import org.simpleflatmapper.util.Predicate; import org.simpleflatmapper.util.TypeHelper; import org.simpleflatmapper.util.TypeReference; import java.io.IOException; import java.lang.reflect.Type; import java.text.Format; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * A CsvWriter allows the caller to write object of type T to an appendable in a specified format. See {@link CsvWriter#from(Class)} to create one. * <p> * The DSL allows to create a CsvWriter easily. The CsvWriter will by default append the headers on the call to {@link CsvWriter.CsvWriterDSL#to(Appendable)} * Because the DSL create a mapper it is better to cache the {@link CsvWriter.CsvWriterDSL}. * <br> * <code> * CsvWriter csvWriter = CsvWriter.from(MyObject.class).to(myWriter);<br> * csvWriter.append(obj1).append(obj2);<br> * </code> * <br> * You can deactivate that by calling {@link CsvWriter.CsvWriterDSL#skipHeaders()} * <br> * <code> * CsvWriter csvWriter = CsvWriter.from(MyObject.class).skipHeaders().to(myWriter);<br> * </code> * <br> * You can also specified the property names. * <br> * <code> * CsvWriter csvWriter = CsvWriter.from(MyObject.class).columns("id", "name").to(myWriter);<br> * </code> * <br> * Or add a property with a specified format * <br> * <code> * CsvWriter csvWriter = CsvWriter.from(MyObject.class).columns("date", new SimpleDateFormat("yyyyMMdd")).to(myWriter);<br> * </code> * * @param <T> the type of object to write */ public class CsvWriter<T> { private final Mapper<T, Appendable> mapper; private final Appendable appendable; private final MappingContext<T> mappingContext; private CsvWriter(Mapper<T, Appendable> mapper, Appendable appendable, MappingContext<T> mappingContext) { this.mapper = mapper; this.appendable = appendable; this.mappingContext = mappingContext; } /** * write the specified value to the underlying appendable. * @param value the value to write * @return the current writer * @throws IOException If an I/O error occurs */ public CsvWriter<T> append(T value) throws IOException { try { mapper.mapTo(value, appendable, mappingContext); } catch(Exception e) { ErrorHelper.rethrow(e); } return this; } /** * Create a DSL on the specified type. * @param type the type of object to write * @param <T> the type * @return a DSL on the specified type */ public static <T> CsvWriterDSL<T> from(Class<T> type) { return from((Type)type); } /** * Create a DSL on the specified type. * @param typeReference the type of object to write * @param <T> the type * @return a DSL on the specified type */ public static <T> CsvWriterDSL<T> from(TypeReference<T> typeReference) { return from(typeReference.getType()); } /** * Create a DSL on the specified type. * @param type the type of object to write * @param <T> the type * @return a DSL on the specified type */ public static <T> CsvWriterDSL<T> from(Type type) { ClassMeta<T> classMeta = ReflectionService.newInstance().getClassMeta(type); CellWriter cellWriter = CsvCellWriter.DEFAULT_WRITER; CsvWriterBuilder<T> builder = CsvWriterBuilder .newBuilder(classMeta, cellWriter); MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig = MapperConfig.<CsvColumnKey>fieldMapperConfig(); try { String[] headers = defaultHeaders(classMeta); for(String header : headers) { builder.addColumn(header); } ContextualMapper<T, Appendable> mapper = (ContextualMapper<T, Appendable>) builder.mapper(); return new DefaultCsvWriterDSL<T>( CsvWriter.<T>toColumnDefinitions(headers), cellWriter, mapper, classMeta, mapperConfig, false); } catch (UnsupportedOperationException e) { return new NoColumnCsvWriterDSL<T>( cellWriter, classMeta, mapperConfig, false); } } private static <T> String[] defaultHeaders(ClassMeta<T> classMeta) { List<String> columns = new ArrayList<String>(); addDefaultHeaders(classMeta, "", columns); return columns.toArray(new String[0]); } private static <P> void addDefaultHeaders(final ClassMeta<P> classMeta, final String prefix, final List<String> columns) { classMeta.forEachProperties(new Consumer<PropertyMeta<P,?>>() { @Override public void accept(PropertyMeta<P, ?> propertyMeta) { if (! PropertyWithGetter.INSTANCE.test(propertyMeta)) return; String currentName = prefix + propertyMeta.getPath(); if (!canWrite(propertyMeta.getPropertyType())) { addDefaultHeaders(propertyMeta.getPropertyClassMeta(), currentName + "_", columns); } else { columns.add(toDelimiterSeparated(currentName)); } } }); } private static String toDelimiterSeparated(String str) { StringBuilder sb = new StringBuilder(str.length()); boolean lastWasUpperCase = false; for(int i = 0; i < str.length(); i ++) { char c = str.charAt(i); if (Character.isUpperCase(c)) { if (lastWasUpperCase) { sb.append(c); } else { if (i > 0) { sb.append('_'); } sb.append(Character.toLowerCase(c)); } lastWasUpperCase = true; } else { lastWasUpperCase = false; sb.append(c); } } return sb.toString(); } private static <P, E> boolean canWrite(Type type) { Converter<? super Object, ?> converter = ConverterService.getInstance().findConverter(type, CharSequence.class); return (converter != null && (! (converter instanceof ToStringConverter) || allowToStringConverter(type) ) ); } private static boolean allowToStringConverter(Type type) { return TypeHelper.isPrimitive(type) || TypeHelper.isEnum(type) || TypeHelper.isInPackage(type, new Predicate<String>() { @Override public boolean test(String s) { return s.startsWith("java."); } }) ; } @SuppressWarnings("unchecked") private static <T> Column[] toColumnDefinitions(String[] header) { Column[] columnDefinitions = new Column[header.length]; int offset = 0; return toColumnDefinitions(header, columnDefinitions, offset); } private static Column[] toColumnDefinitions(String[] header, Column[] columnDefinitions, int offset) { FieldMapperColumnDefinition<CsvColumnKey> identity = FieldMapperColumnDefinition.<CsvColumnKey>identity(); for(int i = 0; i < header.length; i++) { columnDefinitions[i + offset] = new Column(header[i], identity); } return columnDefinitions; } /** * the csv writer DSL * @param <T> the type of object to write */ public static class CsvWriterDSL<T> { protected final Column[] columns; protected final ContextualMapper<T, Appendable> mapper; protected final CellWriter cellWriter; protected final ClassMeta<T> classMeta; protected final MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig; protected final boolean skipHeaders; private CsvWriterDSL( Column[] columns, CellWriter cellWriter, ContextualMapper<T, Appendable> mapper, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { this.columns = columns; this.mapper = mapper; this.cellWriter = cellWriter; this.classMeta = classMeta; this.mapperConfig = mapperConfig; this.skipHeaders = skipHeaders; } /** * Create a writer on the specified appendable for the type T * @param appendable the appendable to write to * @return a CsvWriter on the specified appendable * @throws IOException If an I/O error occurs */ public CsvWriter<T> to(Appendable appendable) throws IOException { if (!skipHeaders) { addHeaders(appendable); } return new CsvWriter<T>(mapper, appendable, mapper.newMappingContext()); } private void addHeaders(Appendable appendable) throws IOException { for(int i = 0; i < columns.length; i++) { if (i != 0) { cellWriter.nextCell(appendable); } cellWriter.writeValue(columns[i].name(), appendable); } cellWriter.endOfRow(appendable); } /** * Create a new DSL object identical to the current one but and append the specified columns * @param columnNames the list of property names * @return the new DSL */ @SuppressWarnings("unchecked") public CsvWriterDSL<T> columns(String... columnNames) { Column[] newColumns = Arrays.copyOf(columns, columns.length + columnNames.length); toColumnDefinitions(columnNames, newColumns, columns.length); return newColumnMapDSL(classMeta, newColumns, mapperConfig, cellWriter, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified property added. * @param column the property name * @param property the property properties * @return the new DSL */ @SuppressWarnings("unchecked") public CsvWriterDSL<T> column(String column, Object... property) { Column[] newColumns = Arrays.copyOf(columns, columns.length + 1); FieldMapperColumnDefinition<CsvColumnKey> columnDefinition = FieldMapperColumnDefinition.<CsvColumnKey>identity().add(property); newColumns[columns.length] = new Column(column, columnDefinition); return newColumnMapDSL(classMeta, newColumns, mapperConfig, cellWriter, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified property added. * @param column the property name * @param format the property formatter * @return the new DSL */ public CsvWriterDSL<T> column(String column, Format format) { return column(column, new FormatProperty(format)); } /** * Create a new DSL object identical to the current one but with the specified classMeta. * @param classMeta the classMeta * @return the new DSL */ public CsvWriterDSL<T> classMeta(ClassMeta<T> classMeta) { return newMapDSL(classMeta, columns, mapperConfig, cellWriter, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified mapperConfig. * @param mapperConfig the mapperConfig * @return the new DSL */ public CsvWriterDSL<T> mapperConfig(MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig) { return newMapDSL(classMeta, columns, mapperConfig, cellWriter, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified cellWriter. * @param cellWriter the cellWriter * @return the new DSL */ public CsvWriterDSL<T> cellWriter(CellWriter cellWriter) { return newMapDSL(classMeta, columns, mapperConfig, cellWriter, skipHeaders); } public CsvWriterDSL<T> separator(char separator) { if (cellWriter instanceof CsvCellWriter) { return newMapDSL(classMeta, columns, mapperConfig, ((CsvCellWriter)cellWriter).separator(separator), skipHeaders); } throw new IllegalStateException("Custom cell writer set, cannot use schema to alter it"); } public CsvWriterDSL<T> quote(char quote) { if (cellWriter instanceof CsvCellWriter) { return newMapDSL(classMeta, columns, mapperConfig, ((CsvCellWriter)cellWriter).quote(quote), skipHeaders); } throw new IllegalStateException("Custom cell writer set, cannot use schema to alter it"); } public CsvWriterDSL<T> endOfLine(String endOfLine) { if (cellWriter instanceof CsvCellWriter) { return newMapDSL(classMeta, columns, mapperConfig, ((CsvCellWriter)cellWriter).endOfLine(endOfLine), skipHeaders); } throw new IllegalStateException("Custom cell writer set, cannot use schema to alter it"); } public CsvWriterDSL<T> alwaysEscape() { if (cellWriter instanceof CsvCellWriter) { return newMapDSL(classMeta, columns, mapperConfig, ((CsvCellWriter)cellWriter).alwaysEscape(), skipHeaders); } throw new IllegalStateException("Custom cell writer set, cannot use schema to alter it"); } /** * Create a new DSL object identical to the current one except it will not append the headers to the appendable. * @return the new DSL */ public CsvWriterDSL<T> skipHeaders() { return newMapDSL(classMeta, columns, mapperConfig, cellWriter, true); } public MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig() { return mapperConfig; } protected CsvWriterDSL<T> newColumnMapDSL( ClassMeta<T> classMeta, Column[] columns, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, CellWriter cellWriter, boolean skipHeaders) { CsvWriterBuilder<T> builder = new CsvWriterBuilder<T>(classMeta, mapperConfig, new FieldMapperToAppendableFactory(cellWriter), cellWriter); for( Column col : columns) { builder.addColumn(col.name(), col.definition()); } ContextualMapper<T, Appendable> mapper = (ContextualMapper<T, Appendable>) builder.mapper(); return new CsvWriterDSL<T>(columns, cellWriter, mapper, classMeta, mapperConfig, skipHeaders); } protected CsvWriterDSL<T> newMapDSL( ClassMeta<T> classMeta, Column[] columns, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, CellWriter cellWriter, boolean skipHeaders) { CsvWriterBuilder<T> builder = new CsvWriterBuilder<T>(classMeta, mapperConfig, new FieldMapperToAppendableFactory(cellWriter), cellWriter); for( Column col : columns) { builder.addColumn(col.name(), col.definition()); } ContextualMapper<T, Appendable> mapper = (ContextualMapper<T, Appendable>) builder.mapper(); return newCsvWriterDSL(columns, cellWriter, mapper, classMeta, mapperConfig, skipHeaders); } protected CsvWriterDSL<T> newCsvWriterDSL(Column[] columns, CellWriter cellWriter, ContextualMapper<T, Appendable> mapper, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { return new CsvWriterDSL<T>(columns, cellWriter, mapper, classMeta, mapperConfig, skipHeaders); } } public static class NoColumnCsvWriterDSL<T> extends CsvWriterDSL<T> { @SuppressWarnings("unchecked") public NoColumnCsvWriterDSL( CellWriter cellWriter, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { super(new Column[0], cellWriter, null, classMeta, mapperConfig, skipHeaders); } @Override public CsvWriter<T> to(Appendable appendable) throws IOException { throw new IllegalStateException("No column defined"); } protected NoColumnCsvWriterDSL<T> newCsvWriterDSL(Column[] columns, CellWriter cellWriter, ContextualMapper<T, Appendable> mapper, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { return new NoColumnCsvWriterDSL<T>(cellWriter, classMeta, mapperConfig, skipHeaders); } } public static class DefaultCsvWriterDSL<T> extends CsvWriterDSL<T> { private DefaultCsvWriterDSL( Column[] columns, CellWriter cellWriter, ContextualMapper<T, Appendable> mapper, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { super(columns, cellWriter, mapper, classMeta, mapperConfig, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified columns instead of the default ones. * @param columnNames the list of property names * @return the new DSL */ public CsvWriterDSL<T> columns(String... columnNames) { return newColumnMapDSL(classMeta, CsvWriter.<T>toColumnDefinitions(columnNames), mapperConfig, cellWriter, skipHeaders); } /** * Create a new DSL object identical to the current one but with the specified property instead of the default ones. * @param column the property name * @param property the property properties * @return the new DSL */ @SuppressWarnings("unchecked") public CsvWriterDSL<T> column(String column, Object... property) { Column[] newColumns = new Column[1]; FieldMapperColumnDefinition<CsvColumnKey> columnDefinition = FieldMapperColumnDefinition.<CsvColumnKey>identity().add(property); newColumns[0] = new Column(column, columnDefinition); return newColumnMapDSL(classMeta, newColumns, mapperConfig, cellWriter, skipHeaders); } protected CsvWriterDSL<T> newCsvWriterDSL(Column[] columns, CellWriter cellWriter, ContextualMapper<T, Appendable> mapper, ClassMeta<T> classMeta, MapperConfig<CsvColumnKey, FieldMapperColumnDefinition<CsvColumnKey>> mapperConfig, boolean skipHeaders) { return new DefaultCsvWriterDSL<T>(columns, cellWriter, mapper, classMeta, mapperConfig, skipHeaders); } } // Tuple2<String, FieldMapperColumnDefinition<CsvColumnKey>> public static class Column { private final String name; private final FieldMapperColumnDefinition<CsvColumnKey> definition; public Column(String name, FieldMapperColumnDefinition<CsvColumnKey> definition) { this.name = name; this.definition = definition; } public String name() { return name; } public FieldMapperColumnDefinition<CsvColumnKey> definition() { return definition; } } }