001package io.konik.csv.mapper; 002 003import com.google.common.base.Function; 004import com.google.common.collect.Lists; 005import org.dozer.DozerBeanMapper; 006import org.dozer.loader.api.*; 007import org.supercsv.cellprocessor.Optional; 008import org.supercsv.cellprocessor.ift.CellProcessor; 009import org.supercsv.io.dozer.CsvDozerBeanData; 010import org.supercsv.io.dozer.CsvDozerBeanReader; 011import org.supercsv.prefs.CsvPreference; 012 013import javax.annotation.Nullable; 014import java.io.*; 015import java.util.Arrays; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.concurrent.CopyOnWriteArrayList; 019 020public class CsvMapperBuilder { 021 022 private final CopyOnWriteArrayList<Column> columns = new CopyOnWriteArrayList<Column>(); 023 024 private final CsvPreference csvPreference; 025 026 public CsvMapperBuilder(CsvPreference csvPreference) { 027 this.csvPreference = csvPreference; 028 } 029 030 public CsvMapperBuilder add(Column.Builder builder) { 031 columns.add(builder.build()); 032 return this; 033 } 034 035 public CsvMapperBuilder addColumns(List<Column> columns) { 036 this.columns.addAll(columns); 037 return this; 038 } 039 040 public CellProcessor[] getCellProcessors() { 041 return Lists.transform(columns, new Function<Column, CellProcessor>() { 042 public CellProcessor apply(Column column) { 043 return column.processor; 044 } 045 }).toArray(new CellProcessor[columns.size()]); 046 } 047 048 public String[] getColumnNames() { 049 return Lists.transform(columns, new Function<Column, String>() { 050 public String apply(Column column) { 051 return column.name; 052 } 053 }).toArray(new String[columns.size()]); 054 } 055 056 public DozerBeanMapper buildBeanMapper(final Class<?> destinationObjectClass) { 057 DozerBeanMapper beanMapper = new DozerBeanMapper(); 058 beanMapper.addMapping(new BeanMappingBuilder() { 059 @Override 060 protected void configure() { 061 TypeMappingBuilder readerBuilder = mapping(CsvDozerBeanData.class, destinationObjectClass, 062 TypeMappingOptions.oneWay(), 063 TypeMappingOptions.wildcard(false), 064 TypeMappingOptions.mapNull(false)); 065 066 TypeMappingBuilder writerBuilder = mapping(destinationObjectClass, 067 type(CsvDozerBeanData.class).mapNull(true), 068 TypeMappingOptions.oneWay(), 069 TypeMappingOptions.wildcard(false)); 070 071 for (int i = 0; i < columns.size(); i++) { 072 Column column = columns.get(i); 073 074 if (column == null) { 075 throw new NullPointerException(String.format("fieldMapping at index %d should not be null", i)); 076 } 077 078 String srcField = "columns["+i+"]"; 079 080 if (column.fieldDefinition != null) { 081 readerBuilder.fields(srcField, column.fieldDefinition, column.mappingOptions); 082 } else { 083 readerBuilder.fields(srcField, column.name, column.mappingOptions); 084 } 085 086 writerBuilder.fields(column.name, srcField, FieldsMappingOptions.copyByReference()); 087 } 088 } 089 }); 090 091 return beanMapper; 092 } 093 094 public static Column.Builder column(String header) { 095 return Column.builder().name(header); 096 } 097 098 public static CsvMapperBuilder withHeadersFromCsvFile(final File csvFile, final ColumnsConfigurer columnsConfigurer) { 099 if (!csvFile.exists()) { 100 throw new IllegalArgumentException("File does not exist!"); 101 } 102 103 CsvPreference csvPreference = recognizeCsvPreference(csvFile); 104 105 try { 106 final CsvDozerBeanReader reader = new CsvDozerBeanReader(new InputStreamReader(new FileInputStream(csvFile), "UTF-8"), csvPreference); 107 final String[] headers = reader.getHeader(true); 108 reader.close(); 109 110 List<Column> columns = Lists.transform(Arrays.asList(headers), new Function<String, Column>() { 111 @Nullable 112 @Override 113 public Column apply(String input) { 114 return columnsConfigurer.getColumnDefinitionForHeader(input); 115 } 116 }); 117 118 return new CsvMapperBuilder(csvPreference).addColumns(columns); 119 120 } catch (Exception e) { 121 throw new RuntimeException("CsvMapperBuilder initialization failed", e); 122 } 123 } 124 125 public static CsvPreference recognizeCsvPreference(File file) { 126 String[] lines = new String[2]; 127 try { 128 BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); 129 int lineNum = 0; 130 String line; 131 132 while ((line = bufferedReader.readLine()) != null && lineNum < 2) { 133 lines[lineNum++] = line.replaceAll("\"([^\"]+)\"", "_"); 134 } 135 136 if (isEmptyLine(lines[0]) || isEmptyLine(lines[1])) { 137 throw new IllegalArgumentException("CSV file has to contain a header and at least one row"); 138 } 139 140 } catch (IOException e) { 141 throw new IllegalStateException("Delimiter recognition failed", e); 142 } 143 144 if (isDelimiter(",", lines[0], lines[1])) { 145 return CsvPreference.STANDARD_PREFERENCE; 146 } 147 148 if (isDelimiter(";", lines[0], lines[1])) { 149 return CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE; 150 } 151 152 throw new IllegalStateException("Delimiter for the CSV file could not be found"); 153 } 154 155 private static boolean isEmptyLine(String line) { 156 return line == null || line.isEmpty(); 157 } 158 159 private static boolean isDelimiter(String delimiter, String lineOne, String lineTwo) { 160 return lineOne.split(delimiter).length == lineTwo.split(delimiter).length && lineOne.contains(delimiter); 161 } 162 163 public CsvDozerBeanReader getBeanReader(File csvFile, Class<?> beanType) { 164 try { 165 CsvDozerBeanReader reader = new CsvDozerBeanReader( 166 new InputStreamReader(new FileInputStream(csvFile), "UTF-8"), 167 csvPreference, 168 buildBeanMapper(beanType) 169 ); 170 reader.getHeader(true); 171 return reader; 172 173 } catch (IOException e) { 174 throw new RuntimeException("Bean reader initialization failed", e); 175 } 176 } 177 178 static class Column { 179 final String name; 180 final Class<?> type; 181 final CellProcessor processor; 182 final FieldsMappingOption[] mappingOptions; 183 final FieldDefinition fieldDefinition; 184 185 public Column(Builder builder) { 186 this.name = builder.name; 187 this.type = builder.type; 188 this.processor = builder.processor; 189 this.mappingOptions = builder.mappingOptions.toArray(new FieldsMappingOption[builder.mappingOptions.size()]); 190 this.fieldDefinition = builder.fieldDefinition; 191 } 192 193 public static Builder builder() { 194 return new Builder(); 195 } 196 197 @Override 198 public String toString() { 199 return "Column{" + 200 "name='" + name + '\'' + 201 ", type=" + type + 202 ", processor=" + processor + 203 ", mappingOptions=" + Arrays.toString(mappingOptions) + 204 ", fieldDefinition=" + fieldDefinition + 205 '}'; 206 } 207 208 public static class Builder { 209 private String name; 210 private Class<?> type = String.class; 211 private CellProcessor processor = new Optional(); 212 private List<FieldsMappingOption> mappingOptions = new LinkedList<FieldsMappingOption>(); 213 private FieldDefinition fieldDefinition; 214 215 public Builder name(String name) { 216 this.name = name; 217 this.fieldDefinition = new FieldDefinition(name).setMethod(String.format("set%s", capitalize(extractChildsField(name)))); 218 return this; 219 } 220 public Builder type(Class<?> type) { 221 this.type = type; 222 return this; 223 } 224 public Builder processor(CellProcessor processor) { 225 this.processor = processor; 226 return this; 227 } 228 public Builder mappingOptions(FieldsMappingOption... mappingOptions) { 229 this.mappingOptions.addAll(Arrays.asList(mappingOptions)); 230 return this; 231 } 232 public Builder fieldDefinition(FieldDefinition fieldDefinition) { 233 this.fieldDefinition = fieldDefinition; 234 return this; 235 } 236 public Column build() { 237 if (mappingOptions.isEmpty()) { 238 mappingOptions.add(FieldsMappingOptions.hintB(type)); 239 } 240 return new Column(this); 241 } 242 243 private static String capitalize(String str) { 244 if (str != null && str.length() > 0) { 245 return str.substring(0,1).toUpperCase() + str.substring(1); 246 } 247 return str; 248 } 249 250 private static String extractChildsField(String path) { 251 if (path.lastIndexOf(".") >= 0) { 252 return path.substring(path.lastIndexOf(".") + 1); 253 } 254 return path; 255 } 256 } 257 } 258}