package io.konik.csv.mapper;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import org.dozer.DozerBeanMapper;
import org.dozer.loader.api.*;
import org.supercsv.cellprocessor.Optional;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.io.dozer.CsvDozerBeanData;
import org.supercsv.io.dozer.CsvDozerBeanReader;
import org.supercsv.prefs.CsvPreference;
import javax.annotation.Nullable;
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CsvMapperBuilder {
private final CopyOnWriteArrayList<Column> columns = new CopyOnWriteArrayList<Column>();
private final CsvPreference csvPreference;
public CsvMapperBuilder(CsvPreference csvPreference) {
this.csvPreference = csvPreference;
}
public CsvMapperBuilder add(Column.Builder builder) {
columns.add(builder.build());
return this;
}
public CsvMapperBuilder addColumns(List<Column> columns) {
this.columns.addAll(columns);
return this;
}
public CellProcessor[] getCellProcessors() {
return Lists.transform(columns, new Function<Column, CellProcessor>() {
public CellProcessor apply(Column column) {
return column.processor;
}
}).toArray(new CellProcessor[columns.size()]);
}
public String[] getColumnNames() {
return Lists.transform(columns, new Function<Column, String>() {
public String apply(Column column) {
return column.name;
}
}).toArray(new String[columns.size()]);
}
public DozerBeanMapper buildBeanMapper(final Class<?> destinationObjectClass) {
DozerBeanMapper beanMapper = new DozerBeanMapper();
beanMapper.addMapping(new BeanMappingBuilder() {
@Override
protected void configure() {
TypeMappingBuilder readerBuilder = mapping(CsvDozerBeanData.class, destinationObjectClass,
TypeMappingOptions.oneWay(),
TypeMappingOptions.wildcard(false),
TypeMappingOptions.mapNull(false));
TypeMappingBuilder writerBuilder = mapping(destinationObjectClass,
type(CsvDozerBeanData.class).mapNull(true),
TypeMappingOptions.oneWay(),
TypeMappingOptions.wildcard(false));
for (int i = 0; i < columns.size(); i++) {
Column column = columns.get(i);
if (column == null) {
throw new NullPointerException(String.format("fieldMapping at index %d should not be null", i));
}
String srcField = "columns["+i+"]";
if (column.fieldDefinition != null) {
readerBuilder.fields(srcField, column.fieldDefinition, column.mappingOptions);
} else {
readerBuilder.fields(srcField, column.name, column.mappingOptions);
}
writerBuilder.fields(column.name, srcField, FieldsMappingOptions.copyByReference());
}
}
});
return beanMapper;
}
public static Column.Builder column(String header) {
return Column.builder().name(header);
}
public static CsvMapperBuilder withHeadersFromCsvFile(final File csvFile, final ColumnsConfigurer columnsConfigurer) {
if (!csvFile.exists()) {
throw new IllegalArgumentException("File does not exist!");
}
CsvPreference csvPreference = recognizeCsvPreference(csvFile);
try {
final CsvDozerBeanReader reader = new CsvDozerBeanReader(new InputStreamReader(new FileInputStream(csvFile), "UTF-8"), csvPreference);
final String[] headers = reader.getHeader(true);
reader.close();
List<Column> columns = Lists.transform(Arrays.asList(headers), new Function<String, Column>() {
@Nullable
@Override
public Column apply(String input) {
return columnsConfigurer.getColumnDefinitionForHeader(input);
}
});
return new CsvMapperBuilder(csvPreference).addColumns(columns);
} catch (Exception e) {
throw new RuntimeException("CsvMapperBuilder initialization failed", e);
}
}
public static CsvPreference recognizeCsvPreference(File file) {
String[] lines = new String[2];
try {
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
int lineNum = 0;
String line;
while ((line = bufferedReader.readLine()) != null && lineNum < 2) {
lines[lineNum++] = line.replaceAll("\"([^\"]+)\"", "_");
}
if (isEmptyLine(lines[0]) || isEmptyLine(lines[1])) {
throw new IllegalArgumentException("CSV file has to contain a header and at least one row");
}
} catch (IOException e) {
throw new IllegalStateException("Delimiter recognition failed", e);
}
if (isDelimiter(",", lines[0], lines[1])) {
return CsvPreference.STANDARD_PREFERENCE;
}
if (isDelimiter(";", lines[0], lines[1])) {
return CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE;
}
throw new IllegalStateException("Delimiter for the CSV file could not be found");
}
private static boolean isEmptyLine(String line) {
return line == null || line.isEmpty();
}
private static boolean isDelimiter(String delimiter, String lineOne, String lineTwo) {
return lineOne.split(delimiter).length == lineTwo.split(delimiter).length && lineOne.contains(delimiter);
}
public CsvDozerBeanReader getBeanReader(File csvFile, Class<?> beanType) {
try {
CsvDozerBeanReader reader = new CsvDozerBeanReader(
new InputStreamReader(new FileInputStream(csvFile), "UTF-8"),
csvPreference,
buildBeanMapper(beanType)
);
reader.getHeader(true);
return reader;
} catch (IOException e) {
throw new RuntimeException("Bean reader initialization failed", e);
}
}
static class Column {
final String name;
final Class<?> type;
final CellProcessor processor;
final FieldsMappingOption[] mappingOptions;
final FieldDefinition fieldDefinition;
public Column(Builder builder) {
this.name = builder.name;
this.type = builder.type;
this.processor = builder.processor;
this.mappingOptions = builder.mappingOptions.toArray(new FieldsMappingOption[builder.mappingOptions.size()]);
this.fieldDefinition = builder.fieldDefinition;
}
public static Builder builder() {
return new Builder();
}
@Override
public String toString() {
return "Column{" +
"name='" + name + '\'' +
", type=" + type +
", processor=" + processor +
", mappingOptions=" + Arrays.toString(mappingOptions) +
", fieldDefinition=" + fieldDefinition +
'}';
}
public static class Builder {
private String name;
private Class<?> type = String.class;
private CellProcessor processor = new Optional();
private List<FieldsMappingOption> mappingOptions = new LinkedList<FieldsMappingOption>();
private FieldDefinition fieldDefinition;
public Builder name(String name) {
this.name = name;
this.fieldDefinition = new FieldDefinition(name).setMethod(String.format("set%s", capitalize(extractChildsField(name))));
return this;
}
public Builder type(Class<?> type) {
this.type = type;
return this;
}
public Builder processor(CellProcessor processor) {
this.processor = processor;
return this;
}
public Builder mappingOptions(FieldsMappingOption... mappingOptions) {
this.mappingOptions.addAll(Arrays.asList(mappingOptions));
return this;
}
public Builder fieldDefinition(FieldDefinition fieldDefinition) {
this.fieldDefinition = fieldDefinition;
return this;
}
public Column build() {
if (mappingOptions.isEmpty()) {
mappingOptions.add(FieldsMappingOptions.hintB(type));
}
return new Column(this);
}
private static String capitalize(String str) {
if (str != null && str.length() > 0) {
return str.substring(0,1).toUpperCase() + str.substring(1);
}
return str;
}
private static String extractChildsField(String path) {
if (path.lastIndexOf(".") >= 0) {
return path.substring(path.lastIndexOf(".") + 1);
}
return path;
}
}
}
}