package com.poiji.bind.mapping; import com.poiji.annotation.ExcelCell; import com.poiji.annotation.ExcelCellName; import com.poiji.annotation.ExcelCellRange; import com.poiji.annotation.ExcelRow; import com.poiji.annotation.ExcelUnknownCells; import com.poiji.config.Casting; import com.poiji.exception.IllegalCastException; import com.poiji.option.PoijiOptions; import com.poiji.util.AnnotationUtil; import com.poiji.util.ReflectUtil; import org.apache.poi.ss.util.CellAddress; import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler; import org.apache.poi.xssf.usermodel.XSSFComment; import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; import static java.lang.String.valueOf; /** * This class handles the processing of a .xlsx file, * and generates a list of instances of a given type * <p> * Created by hakan on 22/10/2017 */ final class PoijiHandler<T> implements SheetContentsHandler { private T instance; private Consumer<? super T> consumer; private int internalRow; private int internalCount; private int limit; private Class<T> type; private PoijiOptions options; private final Casting casting; private Map<String, Integer> columnIndexPerTitle; private Map<Integer, String> titlePerColumnIndex; // New maps used to speed up computing and handle inner objects private Map<String, Object> fieldInstances; private Map<Integer, Field> columnToField; private Map<Integer, Field> columnToSuperClassField; private Set<ExcelCellName> excelCellNames; PoijiHandler(Class<T> type, PoijiOptions options, Consumer<? super T> consumer) { this.type = type; this.options = options; this.consumer = consumer; this.limit = options.getLimit(); casting = options.getCasting(); columnIndexPerTitle = new HashMap<>(); titlePerColumnIndex = new HashMap<>(); columnToField = new HashMap<>(); columnToSuperClassField = new HashMap<>(); excelCellNames = new HashSet<>(); } private void setFieldValue(String content, Class<? super T> subclass, int column) { if (subclass != Object.class) { if (setValue(content, subclass, column)) { return; } setFieldValue(content, subclass.getSuperclass(), column); } } /** * Using this to hold inner objects that will be mapped to the main object **/ private Object getInstance(Field field) { Object ins = null; if (fieldInstances.containsKey(field.getName())) { ins = fieldInstances.get(field.getName()); } else { ins = ReflectUtil.newInstanceOf(field.getType()); fieldInstances.put(field.getName(), ins); } return ins; } private boolean setValue(String content, Class<? super T> type, int column) { Stream.of(type.getDeclaredFields()) .filter(field -> field.getAnnotation(ExcelUnknownCells.class) == null) .forEach(field -> { ExcelRow excelRow = field.getAnnotation(ExcelRow.class); if (excelRow != null) { Object o = casting.castValue(field.getType(), valueOf(internalRow), internalRow, column, options); ReflectUtil.setFieldData(field, o, instance); columnToField.put(-1, field); } ExcelCellRange range = field.getAnnotation(ExcelCellRange.class); if (range != null) { Object ins = null; ins = getInstance(field); for (Field f : field.getType().getDeclaredFields()) { if (setValue(f, column, content, ins)) { ReflectUtil.setFieldData(field, ins, instance); columnToField.put(column, f); columnToSuperClassField.put(column, field); } } } else { if (setValue(field, column, content, instance)) { columnToField.put(column, field); } } }); Stream.of(type.getDeclaredFields()) .filter(field -> field.getAnnotation(ExcelUnknownCells.class) != null) .forEach(field -> { if (!columnToField.containsKey(column)) { try { Map<String, String> excelUnknownCellsMap; field.setAccessible(true); if (field.get(instance) == null) { excelUnknownCellsMap = new HashMap<>(); ReflectUtil.setFieldData(field, excelUnknownCellsMap, instance); } else { excelUnknownCellsMap = (Map) field.get(instance); } excelUnknownCellsMap.put(titlePerColumnIndex.get(column), content); } catch (IllegalAccessException e) { throw new IllegalCastException("Could not read content of field " + field.getName() + " on Object {" + instance + "}"); } } }); // For ExcelRow annotation if (columnToField.containsKey(-1)) { Field field = columnToField.get(-1); Object o = casting.castValue(field.getType(), valueOf(internalRow), internalRow, column, options); ReflectUtil.setFieldData(field, o, instance); } if (columnToField.containsKey(column) && columnToSuperClassField.containsKey(column)) { Field field = columnToField.get(column); Object ins; ins = getInstance(columnToSuperClassField.get(column)); if (setValue(field, column, content, ins)) { ReflectUtil.setFieldData(columnToSuperClassField.get(column), ins, instance); return true; } return setValue(field, column, content, instance); } return false; } private boolean setValue(Field field, int column, String content, Object ins) { ExcelCell index = field.getAnnotation(ExcelCell.class); if (index != null) { Class<?> fieldType = field.getType(); if (column == index.value()) { Object o = casting.castValue(fieldType, content, internalRow, column, options); ReflectUtil.setFieldData(field, o, ins); return true; } } else { ExcelCellName excelCellName = field.getAnnotation(ExcelCellName.class); if (excelCellName != null) { excelCellNames.add(excelCellName); Class<?> fieldType = field.getType(); final String titleName = options.getCaseInsensitive() ? excelCellName.value().toLowerCase() : excelCellName.value(); final Integer titleColumn = columnIndexPerTitle.get(titleName); //Fix both columns mapped to name passing this condition below if (titleColumn != null && titleColumn == column) { Object o = casting.castValue(fieldType, content, internalRow, column, options); ReflectUtil.setFieldData(field, o, ins); return true; } } } return false; } @Override public void startRow(int rowNum) { if (rowNum + 1 > options.skip()) { internalCount += 1; instance = ReflectUtil.newInstanceOf(type); fieldInstances = new HashMap<>(); } } @Override public void endRow(int rowNum) { if (internalRow != rowNum) return; if (rowNum + 1 > options.skip()) { consumer.accept(instance); } } @Override public void cell(String cellReference, String formattedValue, XSSFComment comment) { CellAddress cellAddress = new CellAddress(cellReference); int row = cellAddress.getRow(); int headers = options.getHeaderStart(); int column = cellAddress.getColumn(); if (row <= headers) { columnIndexPerTitle.put( options.getCaseInsensitive() ? formattedValue.toLowerCase() : formattedValue, column ); titlePerColumnIndex.put(column, getTitleNameForMap(formattedValue, column)); } if (row + 1 <= options.skip()) { return; } if (limit != 0 && internalCount > limit) { return; } internalRow = row; setFieldValue(formattedValue, type, column); } private String getTitleNameForMap(String cellContent, int columnIndex) { String titleName; if (titlePerColumnIndex.containsValue(cellContent) || cellContent.isEmpty()) { titleName = cellContent + "@" + columnIndex; } else { titleName = cellContent; } return titleName; } @Override public void headerFooter(String text, boolean isHeader, String tagName) { //no-op } @Override public void endSheet() { AnnotationUtil.validateMandatoryNameColumns(options, type, columnIndexPerTitle.keySet()); } }