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}