package org.sep4j; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.apache.commons.lang.time.DateFormatUtils; import org.apache.poi.hssf.usermodel.HSSFCellStyle; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.sep4j.support.FunctionThrowingParseException; import org.sep4j.support.SepBasicTypeConverts; import org.sep4j.support.SepConstants; import org.sep4j.support.SepRecordType; import org.sep4j.support.SepReflectionHelper; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import static org.sep4j.HeaderUtils.generateReverseHeaderMapFromProps; /** * The facade to do records saving and retrieving. Ssio = SpreadSheet * Input/Output * * @author chenjianjx * * */ public class Ssio { private Ssio(){ } /** * please check the doc of {@link #save(Map, Collection, OutputStream)} . * The difference here is that the headerMap is automatically worked out * with a rule like {"firstName" (a property in the record class) => "First * Name" (a spreadsheet header column) }. For details please check * {@link HeaderUtils#generateHeaderMapFromProps(Class)} * @param recordClass * @param records * @param outputStream */ public static <T> void save(Class<T> recordClass, Collection<T> records, OutputStream outputStream) { doSave(HeaderUtils.generateHeaderMapFromProps(recordClass), records, SepRecordType.JAVABEAN, outputStream, null, null, true); } /** * please check the doc of {@link #saveMaps(Map, Collection, OutputStream)} */ public static void saveMaps(Collection<String> keys, Collection<Map<String, Object>> records, OutputStream outputStream) { saveMaps(HeaderUtils.generateHeaderMapFromPropNames(keys), records, outputStream); } /** * save records to a new workbook even if there are datum errors in the * records. Any datum error will lead to an empty cell. * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param <T> * the java type of records * */ public static <T> void save(Map<String, String> headerMap, Collection<T> records, OutputStream outputStream) { save(headerMap, records, outputStream, null); } /** * please check the doc of {@link #saveMaps(Map, Collection, OutputStream, String, List)} */ public static void saveMaps(Map<String, String> headerMap, Collection<Map<String, Object>> records, OutputStream outputStream) { saveMaps(headerMap, records, outputStream, null, null); } /** * please check the doc of {@link #save(Map, Collection, OutputStream)} * @param headerMap * @param records * @param outputFile */ public static <T> void save(Map<String, String> headerMap, Collection<T> records, File outputFile) { consumeAsOutputStream(outputFile, outputStream -> save(headerMap, records, outputStream)); } /** * please check the doc of {@link #saveMaps(Map, Collection, OutputStream)} */ public static void saveMaps(Map<String, String> headerMap, Collection<Map<String, Object>> records, File outputFile) { consumeAsOutputStream(outputFile, outputStream -> saveMaps(headerMap, records, outputStream)); } /** * please check the doc of {@link #save(Class, Collection, OutputStream)} * @param recordClass * @param records * @param outputFile */ public static <T> void save(Class<T> recordClass, Collection<T> records, File outputFile) { consumeAsOutputStream(outputFile, outputStream -> save(recordClass, records, outputStream)); } /** * please check the doc of {@link #saveMaps(Map, Collection, File)} */ public static void saveMaps(Collection<String> keys, Collection<Map<String, Object>> records, File outputFile) { consumeAsOutputStream(outputFile, outputStream -> saveMaps(keys, records, outputStream)); } /** * save records to a new workbook even if there are datum errors in the * records. Any datum error will lead to datumErrPlaceholder being written * to the cell. * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param <T> * the java type of records */ public static <T> void save(Map<String, String> headerMap, Collection<T> records, OutputStream outputStream, String datumErrPlaceholder) { doSave(headerMap, records, SepRecordType.JAVABEAN, outputStream, datumErrPlaceholder, null, true); } /** * save records to a new workbook even if there are datum errors in the * records. Any datum error will lead to datumErrPlaceholder being written * to the cell. All the datum errors will be saved to datumErrors indicating * the recordIndex of the datum * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * * @param <T> * the java type of records */ public static <T> void save(Map<String, String> headerMap, Collection<T> records, OutputStream outputStream, String datumErrPlaceholder, List<DatumError> datumErrors) { doSave(headerMap, records, SepRecordType.JAVABEAN, outputStream, datumErrPlaceholder, datumErrors, true); } /** * please check the doc of {@link #save(Map, Collection, OutputStream, String, List)} * @param headerMap * @param records * @param outputFile * @param datumErrPlaceholder * @param datumErrors */ public static <T> void save(Map<String, String> headerMap, Collection<T> records, File outputFile, String datumErrPlaceholder, List<DatumError> datumErrors) { consumeAsOutputStream(outputFile, outputStream -> doSave(headerMap, records, SepRecordType.JAVABEAN, outputStream, datumErrPlaceholder, datumErrors, true)); } /** * save a collection of maps to a new workbook even if there are datum errors in the * records. Any datum error will lead to datumErrPlaceholder being written * to the cell. All the datum errors will be saved to datumErrors indicating * the recordIndex of the datum * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * */ public static void saveMaps(Map<String, String> headerMap, Collection<Map<String, Object>> records, OutputStream outputStream, String datumErrPlaceholder, List<DatumError> datumErrors) { doSave(headerMap, records, SepRecordType.MAP, outputStream, datumErrPlaceholder, datumErrors, true); } /** * please check the doc of {@link #saveMaps(Map, Collection, File, String, List)} */ public static void saveMaps(Map<String, String> headerMap, Collection<Map<String, Object>> records, File outputFile, String datumErrPlaceholder, List<DatumError> datumErrors) { consumeAsOutputStream(outputFile, outputStream -> saveMaps(headerMap, records, outputStream, datumErrPlaceholder, datumErrors)); } /** * please check the doc of {@link #appendTo(Map, Collection, File, String, List)} */ public static <T> void appendTo(Map<String, String> headerMap, Collection<T> records, File file) { appendTo(headerMap, records, file, null, null); } /** * append records to an existing spreadsheet file * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * Note: the number and sequence of the map's keys have to be the same as the header of the existing spreadsheet file * @param records * the records to save. * @param file * the exiting file to append to * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * * @param <T> * the java type of records */ public static <T> void appendTo(Map<String, String> headerMap, Collection<T> records, File file, String datumErrPlaceholder, List<DatumError> datumErrors) { doAppend(headerMap, records, SepRecordType.JAVABEAN, file, datumErrPlaceholder, datumErrors); } /** * append a collection of Maps to an existing spreadsheet file * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * Note: the number and sequence of the map's keys have to be the same as the header of the existing spreadsheet file * @param records * the records to save. * @param file * the exiting file to append to * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * */ public static void appendMapsTo(Map<String, String> headerMap, Collection<Map<String, Object>> records, File file, String datumErrPlaceholder, List<DatumError> datumErrors) { doAppend(headerMap, records, SepRecordType.MAP, file, datumErrPlaceholder, datumErrors); } /** * append records to an existing spreadsheet file * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param recordType the type of data structure of the record * * @param file * the file to append to * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * * @param <T> * the java type of records */ private static <T> void doAppend(Map<String, String> headerMap, Collection<T> records, SepRecordType recordType, File file, String datumErrPlaceholder, List<DatumError> datumErrors) { validateHeaderMap(headerMap); if (records == null) { records = new ArrayList<T>(); } Workbook workbook; try(InputStream inputStream = new FileInputStream(file)){ workbook = toWorkbook(inputStream); if (workbook.getNumberOfSheets() <= 0) { throw new IllegalArgumentException("There is no sheet in file " + file); } Sheet sheet = workbook.getSheetAt(0); int lastRowNum = sheet.getLastRowNum(); //1-based int recordIndex = 0; int rowIndex = lastRowNum + 1; for (T record : records) { createRow(headerMap, record, recordType, recordIndex, sheet, rowIndex, datumErrPlaceholder, datumErrors); recordIndex++; rowIndex++; } } catch (IOException e) { throw new IllegalStateException(e); } catch (InvalidFormatException e) { throw new IllegalStateException(e); } try(OutputStream outputStream = new FileOutputStream(file)){ workbook.write(outputStream); } catch (IOException e) { throw new IllegalStateException(e); } } /** * save records to a new workbook only if there are no datum errors in the * records. Any datum error will lead to datumErrPlaceholder being written * to the cell. All the datum errors will be saved to datumErrors indicating * the recordIndex of the datum * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * * @param <T> * the java type of records */ public static <T> void saveIfNoDatumError( Map<String, String> headerMap, Collection<T> records, OutputStream outputStream, String datumErrPlaceholder, List<DatumError> datumErrors) { doSave(headerMap, records, SepRecordType.JAVABEAN, outputStream, datumErrPlaceholder, datumErrors, false); } /** * please check the doc of {@link #parse(Map, InputStream, List, Class)}. * The difference is that this class ignore any all the errors and make sure * no checked exception is thrown(There may still be unchecked exceptions, * though). * * @param reverseHeaderMap * {@code <headerText, propName>, for example <"User Name" as the spreadsheet header, "username" of User class>.} * @param inputStream * the input stream of this spreadsheet * @param recordClass the class the java bean. It must have a default constructor * @param <T> * the java type of records * @return a list of beans */ public static <T> List<T> parseIgnoringErrors( Map<String, String> reverseHeaderMap, InputStream inputStream, Class<T> recordClass) { try { return parse(reverseHeaderMap, inputStream, null, recordClass); } catch (InvalidFormatException e1) { // ignore return new ArrayList<T>(); } catch (InvalidHeaderRowException e1) { // ignore return new ArrayList<T>(); } } /** * please check the doc of {@link #parseToMaps(Map, InputStream, List)} */ public static List<Map<String, String>> parseToMapsIgnoringErrors( Map<String, String> reverseHeaderMap, InputStream inputStream) { try { return parseToMaps(reverseHeaderMap, inputStream, null); } catch (InvalidFormatException e1) { // ignore return new ArrayList<Map<String, String>>(); } catch (InvalidHeaderRowException e1) { // ignore return new ArrayList<Map<String, String>>(); } } /** * Please check the doc of * {@link #parseIgnoringErrors(Map, InputStream, Class)}. The difference * here is that the reverseHeaderMap is automatically worked out with a rule * like {"First Name" (a spreadsheet header) => "firstName" (a property * in the record class) }. For details please check {@link HeaderUtils#generateReverseHeaderMapFromProps(Class)} * * @param inputStream * @param recordClass * @return */ public static <T> List<T> parseIgnoringErrors(InputStream inputStream, Class<T> recordClass) { return parseIgnoringErrors(generateReverseHeaderMapFromProps(recordClass), inputStream, recordClass); } /** * Please check the doc of {@link #parseIgnoringErrors(InputStream, Class)} * @param inputFile * @param recordClass * @return */ public static <T> List<T> parseIgnoringErrors(File inputFile, Class<T> recordClass) { return readAsInputStream(inputFile, input -> parseIgnoringErrors(input, recordClass)); } /** * please check the doc of {@link #parseIgnoringErrors(Map, InputStream, Class)}. * @param reverseHeaderMap * @param inputFile * @param recordClass * @return */ public static <T> List<T> parseIgnoringErrors( Map<String, String> reverseHeaderMap, File inputFile, Class<T> recordClass) { return readAsInputStream(inputFile, input -> parseIgnoringErrors(reverseHeaderMap, input, recordClass)); } /** * please check the doc of {@link #parseToMapsIgnoringErrors(Map, InputStream)} */ public static List<Map<String, String>> parseToMapsIgnoringErrors( Map<String, String> reverseHeaderMap, File inputFile) { return readAsInputStream(inputFile, input -> parseToMapsIgnoringErrors(reverseHeaderMap, input)); } /** * <p>parse an spreadsheet to a list of beans. </p> * The columns are not identified by the column indexes, but by the header * rows' text of the columns specified by parameter reverseHeaderMap , i.e. * you don't have to worry which column to put "username". All you need to * do is to let the spreadsheet have a header column named "User Name" and * associate it with "username" property in parameter reverseHeaderMap * * @param reverseHeaderMap * {@code <headerText, propName>, for example <"User Name" as the spreadsheet header, "username" of User class>.} * @param inputStream * the input stream of this spreadsheet * @param cellErrors * the errors of data rows (not including header row) found while * being parsed. The error here can tell you which cell is wrong. * @param recordClass * the class the java bean. It must have a default constructor * @param <T> * the java type of records * @return a list of beans * @throws InvalidFormatException * the input stream doesn't represent a valid spreadsheet * @throws InvalidHeaderRowException * the header row of the spreadsheet is not valid, for example, * no headerText accords to that of the reverseHeaerMap */ public static <T> List<T> parse(Map<String, String> reverseHeaderMap, InputStream inputStream, List<CellError> cellErrors, Class<T> recordClass) throws InvalidFormatException, InvalidHeaderRowException { validateRecordClass(recordClass); return doParse(reverseHeaderMap, inputStream, recordClass, SepRecordType.JAVABEAN, cellErrors); } /** * <p>parse an spreadsheet to a list of Map<String,String>. </p> * The columns are not identified by the column indexes, but by the header * rows' text of the columns specified by parameter reverseHeaderMap , i.e. * you don't have to worry which column to put "username". All you need to * do is to let the spreadsheet have a header column named "User Name" and * associate it with "username" property in parameter reverseHeaderMap * * @param reverseHeaderMap * {@code <headerText, propName>, for example <"User Name" as the spreadsheet header, "username" of User class>.} * @param inputStream * the input stream of this spreadsheet * @param cellErrors * the errors of data rows (not including header row) found while * being parsed. The error here can tell you which cell is wrong. * @return a list of Map<String,String> Please note Date cells in the spreadsheet will be formatted using {@link SepConstants#DEAULT_DATE_FORMAT} * @throws InvalidFormatException * the input stream doesn't represent a valid spreadsheet * @throws InvalidHeaderRowException * the header row of the spreadsheet is not valid, for example, * no headerText accords to that of the reverseHeaerMap */ public static List<Map<String, String>> parseToMaps(Map<String, String> reverseHeaderMap, InputStream inputStream, List<CellError> cellErrors) throws InvalidFormatException, InvalidHeaderRowException { return doParse(reverseHeaderMap, inputStream, null, SepRecordType.MAP, cellErrors); } private static <T> List<T> doParse(Map<String, String> reverseHeaderMap, InputStream inputStream, Class<T> recordClassIfJavaBean, SepRecordType recordType, List<CellError> cellErrors) throws InvalidFormatException, InvalidHeaderRowException { validateReverseHeaderMap(reverseHeaderMap); Workbook workbook = toWorkbook(inputStream); if (workbook.getNumberOfSheets() <= 0) { return new ArrayList<T>(); } Sheet sheet = workbook.getSheetAt(0); // key = columnIndex, value= {propName, headerText} Map<Short, ColumnMeta> columnMetaMap = parseHeader(reverseHeaderMap, sheet.getRow(0)); if (columnMetaMap.isEmpty()) { throw new InvalidHeaderRowException(); } // now do the data rows List<T> records = new ArrayList<T>(); for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { Row row = sheet.getRow(rowIndex); if (row == null) { continue; } T record = (T) parseDataRow(columnMetaMap, row, rowIndex, recordType, recordClassIfJavaBean, cellErrors); records.add(record); } return records; } /** * please check the doc of {@link #parse(Map, InputStream, List, Class)} * @param reverseHeaderMap * @param inputFile * @param cellErrors * @param recordClass * @return * @throws InvalidFormatException * @throws InvalidHeaderRowException */ public static <T> List<T> parse(Map<String, String> reverseHeaderMap, File inputFile, List<CellError> cellErrors, Class<T> recordClass) throws InvalidFormatException, InvalidHeaderRowException { return readAsInputStreamThrowingParseException(inputFile, input -> parse(reverseHeaderMap, input, cellErrors, recordClass)); } /** * save records to a new workbook. * * @param headerMap * {@code <propName, headerText>, for example <"username" field of User class, "User Name" as the spreadsheet header text>. } * @param records * the records to save. * @param outputStream * the output stream for the spreadsheet * @param datumErrPlaceholder * if some datum is wrong, write this place holder to the cell * (stillSaveIfDataError should be set true) * @param datumErrors * all data errors in the records * @param stillSaveIfDataError * if there are errors in data, should we still save the records * ? * * */ static <T> void doSave(Map<String, String> headerMap, Collection<T> records, SepRecordType recordType, OutputStream outputStream, String datumErrPlaceholder, List<DatumError> datumErrors, boolean stillSaveIfDataError) { validateHeaderMap(headerMap); if (records == null) { records = new ArrayList<T>(); } if (outputStream == null) { throw new IllegalArgumentException("the outputStream can not be null"); } Workbook wb = new XSSFWorkbook(); Sheet sheet = wb.createSheet(); createHeaders(headerMap, sheet); int recordIndex = 0; for (T record : records) { int rowIndex = recordIndex + 1; createRow(headerMap, record, recordType, recordIndex, sheet, rowIndex, datumErrPlaceholder, datumErrors); recordIndex++; } if (shouldSave(datumErrors, stillSaveIfDataError)) { writeWorkbook(wb, outputStream); } } /** * the workbook has been generated. Should we write it to the outputstream? */ static boolean shouldSave(List<DatumError> datumErrors, boolean stillSaveIfDataError) { if (stillSaveIfDataError) { return true; } if (datumErrors == null || datumErrors.isEmpty()) { return true; } else { return false; } } static void validateRecordClass(Class<?> recordClass) { if (recordClass == null) { throw new IllegalArgumentException( "the recordClass can not be null"); } } static void validateReverseHeaderMap(Map<String, String> reverseHeaderMap) { if (reverseHeaderMap == null || reverseHeaderMap.isEmpty()) { throw new IllegalArgumentException( "the reverseHeaderMap can not be null or empty"); } int columnIndex = 0; for (Map.Entry<String, String> entry : reverseHeaderMap.entrySet()) { String headerText = entry.getKey(); String propName = entry.getValue(); if (StringUtils.isBlank(headerText)) { throw new IllegalArgumentException( "One header defined in the reverseHeaderMap has a blank headerText. Header Index (0-based) = " + columnIndex); } if (StringUtils.isBlank(propName)) { throw new IllegalArgumentException( "One header defined in the reverseHeaderMap has a blank propName. Header Index (0-based) = " + columnIndex); } columnIndex++; } } static void validateHeaderMap(Map<String, String> headerMap) { if (headerMap == null || headerMap.isEmpty()) { throw new IllegalArgumentException( "the headerMap can not be null or empty"); } int columnIndex = 0; for (Map.Entry<String, String> entry : headerMap.entrySet()) { String propName = entry.getKey(); if (StringUtils.isBlank(propName)) { throw new IllegalArgumentException( "One header has a blank propName. Header Index (0-based) = " + columnIndex); } columnIndex++; } } static <T> void setPropertyWithCellValue(Class<T> recordClass, T record, String propName, Object cellStringOrDate) { IllegalArgumentException noSetterException = new IllegalArgumentException( MessageFormat .format("No suitable setter for property \"{0}\" with cellValue \"{1}\" ", propName, cellStringOrDate)); List<Method> setters = SepReflectionHelper.findSettersByPropName( recordClass, propName); // no setter for this prop if (setters.isEmpty()) { throw noSetterException; } if (cellStringOrDate == null) { // in this case, try all the setters one by one for (Method setter : setters) { Class<?> propClass = setter.getParameterTypes()[0]; if (SepBasicTypeConverts.canFromNull(propClass)) { SepReflectionHelper.invokeSetter(setter, record, null); return; } } throw noSetterException; } if (cellStringOrDate instanceof java.util.Date) { Method setter = SepReflectionHelper.findSetterByPropNameAndType( recordClass, propName, java.util.Date.class); if (setter == null) { throw noSetterException; } else { SepReflectionHelper.invokeSetter(setter, record, cellStringOrDate); return; } } // ok, we got a string String cellText = (String) cellStringOrDate; // try to find a string-type setter first Method stringSetter = SepReflectionHelper.findSetterByPropNameAndType( recordClass, propName, String.class); if (stringSetter != null) { SepReflectionHelper.invokeSetter(stringSetter, record, cellText); return; } // no string-type setter? do a guess! for (Method setter : setters) { Class<?> propClass = setter.getParameterTypes()[0]; if (SepBasicTypeConverts.canFromThisString(cellText, propClass)) { Object propValue = SepBasicTypeConverts.fromThisString( cellText, propClass); SepReflectionHelper.invokeSetter(setter, record, propValue); return; } } throw noSetterException; } static <T> T createRecordInstance(Class<T> recordClass) { try { Constructor<T> constructor = recordClass .getDeclaredConstructor(new Class[0]); constructor.setAccessible(true); return constructor.newInstance(new Object[0]); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } /** * read the cell. it only supports: boolean, numeric, date(numeric cell type * + date cell format) and string. * * @param cell * the cell to read * @return the date if it is a date cell, or else the string value (will be * trimmed to null) . <br/> * * */ static Object readCellAsStringOrDate(Cell cell) { if (cell == null) { return null; } if (cell.getCellType() == Cell.CELL_TYPE_BLANK) { return null; } if (cell.getCellType() == Cell.CELL_TYPE_BOOLEAN) { return String.valueOf(cell.getBooleanCellValue()); } if (cell.getCellType() == Cell.CELL_TYPE_ERROR) { return null; } if (cell.getCellType() == Cell.CELL_TYPE_FORMULA) { return null; } if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) { if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } else { double v = cell.getNumericCellValue(); return String.valueOf(v); } } if (cell.getCellType() == Cell.CELL_TYPE_STRING) { String s = cell.getStringCellValue(); return StringUtils.trimToNull(s); } return null; } private static Object parseDataRow(Map<Short, ColumnMeta> columnMetaMap, Row row, int rowIndex, SepRecordType recordType, Class recordClassIfJavaBean, List<CellError> cellErrors) { Object record; switch (recordType){ case JAVABEAN: record = createRecordInstance(recordClassIfJavaBean); break; case MAP: record = new LinkedHashMap<String, String>(); break; default: throw new IllegalArgumentException("Unsupported record type: " + recordType); } for (short columnIndex = 0; columnIndex < row.getLastCellNum(); columnIndex++) { ColumnMeta columnMeta = columnMetaMap.get(columnIndex); if (columnMeta == null || columnMeta.propName == null) { continue; } String propName = columnMeta.propName; Cell cell = row.getCell(columnIndex); Object cellStringOrDate = readCellAsStringOrDate(cell); switch (recordType){ case JAVABEAN: try { setPropertyWithCellValue(recordClassIfJavaBean, record, propName, cellStringOrDate); } catch (Exception e) { if (cellErrors != null) { CellError ce = new CellError(); ce.setColumnIndex(columnIndex); ce.setHeaderText(columnMeta.headerText); ce.setPropName(propName); ce.setRowIndex(rowIndex); ce.setCause(e); cellErrors.add(ce); } } break; case MAP: Map<String, String> map = (Map<String, String>) record; String value; if (cellStringOrDate instanceof Date) { value = DateFormatUtils.format(((Date) cellStringOrDate), SepConstants.DEAULT_DATE_FORMAT); } else { value = cellStringOrDate == null? null: cellStringOrDate.toString(); } map.put(propName, value); break; default: throw new IllegalArgumentException("Unsupported record type: " + recordType); } } return record; } /** * meta info about a column * * */ private static class ColumnMeta { public String propName; public String headerText; @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } /** * to get <columnIndex, column info> */ private static Map<Short, ColumnMeta> parseHeader( Map<String, String> reverseHeaderMap, Row row) { Map<Short, ColumnMeta> columnMetaMap = new LinkedHashMap<Short, ColumnMeta>(); // note that row.getLastCellNum() is one-based for (short columnIndex = 0; columnIndex < row.getLastCellNum(); columnIndex++) { Cell cell = row.getCell(columnIndex); Object headerObj = readCellAsStringOrDate(cell); String headerText = headerObj == null ? "" : headerObj.toString(); if (headerText == null) { continue; } String propName = reverseHeaderMap.get(headerText); if (propName == null) { continue; } ColumnMeta cm = new ColumnMeta(); cm.headerText = headerText; cm.propName = propName; columnMetaMap.put(columnIndex, cm); } return columnMetaMap; } private static Row createHeaders(Map<String, String> headerMap, Sheet sheet) { CellStyle style = sheet.getWorkbook().createCellStyle(); style.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); style.setFillPattern(CellStyle.SOLID_FOREGROUND); style.setBorderBottom(HSSFCellStyle.BORDER_THIN); style.setBorderTop(HSSFCellStyle.BORDER_THIN); style.setBorderRight(HSSFCellStyle.BORDER_THIN); style.setBorderLeft(HSSFCellStyle.BORDER_THIN); Row header = sheet.createRow(0); int columnIndex = 0; for (Map.Entry<String, String> entry : headerMap.entrySet()) { String headerText = StringUtils.defaultString(entry.getValue()); Cell cell = createCell(header, columnIndex); cell.setCellValue(headerText); cell.setCellStyle(style); sheet.autoSizeColumn(columnIndex); columnIndex++; } return header; } private static <T> Row createRow(Map<String, String> headerMap, T record, SepRecordType recordType, int recordIndex, Sheet sheet, int rowIndex, String datumErrPlaceholder, List<DatumError> datumErrors) { Row row = sheet.createRow(rowIndex); int columnIndex = 0; for (Map.Entry<String, String> entry : headerMap.entrySet()) { boolean datumErr = false; String propName = entry.getKey(); Object propValue = null; try { propValue = getProperty(record, recordType, propName); } catch (Exception e) { if (datumErrors != null) { DatumError de = new DatumError(); de.setPropName(propName); de.setRecordIndex(recordIndex); de.setCause(e); datumErrors.add(de); } datumErr = true; propValue = datumErrPlaceholder; } String propValueText = (propValue == null ? null : propValue .toString()); Cell cell = createCell(row, columnIndex); cell.setCellValue(StringUtils.defaultString(propValueText)); if (datumErr) { CellStyle errStyle = sheet.getWorkbook().createCellStyle(); errStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); errStyle.setFillPattern(CellStyle.SOLID_FOREGROUND); cell.setCellStyle(errStyle); } columnIndex++; } return row; } private static <T> Object getProperty(T record, SepRecordType recordType, String propName) { switch(recordType){ case JAVABEAN: { return SepReflectionHelper.getProperty(record, propName); } case MAP: { Map map = (Map) record; return map.get(propName); } default: { throw new IllegalArgumentException("Unsupported record type: " + recordType); } } } private static Cell createCell(Row row, int columnIndex) { Cell cell = row.createCell(columnIndex); return cell; } private static void writeWorkbook(Workbook workbook, OutputStream outputStream) { try { workbook.write(outputStream); } catch (IOException e) { throw new IllegalStateException(e); } } private static Workbook toWorkbook(InputStream inputStream) throws InvalidFormatException { try { return WorkbookFactory.create(inputStream); } catch (IOException e) { throw new RuntimeException(e); } } /** * use this template to avoid try/catch in every method. * This way the test converage rate can also be increased, * otherwise every catch statement in every method is normally covered by tests * * @param file * @param action */ private static void consumeAsOutputStream(File file, Consumer<OutputStream> action) { try (OutputStream output = new FileOutputStream(file)) { action.accept(output); } catch (IOException e) { throw new IllegalStateException(e); } } /** * use this template to avoid try/catch in every method. * This way the test converage rate can also be increased, * otherwise every catch statement in every method is normally covered by tests * * @param file * @param action */ private static <T> T readAsInputStreamThrowingParseException(File file, FunctionThrowingParseException<InputStream, T> action) throws InvalidFormatException, InvalidHeaderRowException { try (InputStream input = new FileInputStream(file)) { return action.applyThrows(input); } catch (IOException e) { throw new IllegalStateException(e); } catch (InvalidFormatException e) { throw e; } catch (InvalidHeaderRowException e) { throw e; } } private static <T> T readAsInputStream(File file, Function<InputStream, T> action) { try (InputStream input = new FileInputStream(file)) { return action.apply(input); } catch (IOException e) { throw new IllegalStateException(e); } } }