diff --git a/README.md b/README.md index ac951c1..1f7757f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This library is available in [Maven Central](https://mvnrepository.com/artifact/ io.github.millij poi-object-mapper - 3.0.0 + 3.1.0 ``` @@ -76,12 +76,25 @@ Reading spreadsheet rows as objects .. ```java ... - final File xlsxFile = new File(""); + final File xlsFile = new File(""); final XlsReader reader = new XlsReader(); - final List employees = reader.read(Employee.class, xlsxFile); + final List employees = reader.read(Employee.class, xlsFile); + ... +``` + +##### Reading Rows as Map (when there is no mapping bean) + +Reading spreadsheet rows as `Map` Objects .. + +```java + ... + final File xlsxFile = new File(""); + final XlsxReader reader = new XlsxReader(); // OR XlsReader as needed + final List> rowObjects = reader.read(xlsxFile); ... ``` + ##### Writing a collection of objects to file Similar to `Reader`, the mapped Java Beans can be written to files. diff --git a/build.gradle b/build.gradle index a45efbc..5151abf 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ sourceCompatibility = 1.11 targetCompatibility = 1.11 group = 'io.github.millij' -version = '3.0.0' +version = '3.1.0' dependencies { @@ -37,6 +37,9 @@ dependencies { // Test compile // ---------------------------------------------------------------------------------- + testImplementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.19.0' + testImplementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.19.0' + testImplementation group: 'junit', name: 'junit', version: '4.12' } diff --git a/src/main/java/io/github/millij/poi/ss/handler/RowContentsAsMapHandler.java b/src/main/java/io/github/millij/poi/ss/handler/RowContentsAsMapHandler.java new file mode 100644 index 0000000..01a74dc --- /dev/null +++ b/src/main/java/io/github/millij/poi/ss/handler/RowContentsAsMapHandler.java @@ -0,0 +1,123 @@ +package io.github.millij.poi.ss.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.millij.poi.util.Spreadsheet; + + +/** + * SheetContentsHandler impl for reading row as {@link Map} + * + * @since 3.1.0 + */ +public class RowContentsAsMapHandler extends AbstractSheetContentsHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RowContentsAsMapHandler.class); + + private final RowListener> listener; + + private final int headerRowNum; + private final Map headerCellRefsMap; + + private final int lastRowNum; + + + // Constructors + // ------------------------------------------------------------------------ + + public RowContentsAsMapHandler(RowListener> listener, int headerRowNum, int lastRowNum) { + super(); + + // init + this.listener = listener; + + this.headerRowNum = headerRowNum; + this.headerCellRefsMap = new HashMap<>(); + + this.lastRowNum = lastRowNum; + } + + + // AbstractSheetContentsHandler Methods + // ------------------------------------------------------------------------ + + @Override + void beforeRowStart(final int rowNum) { + try { + // Row Callback + listener.beforeRow(rowNum); + } catch (Exception ex) { + String errMsg = String.format("Error calling #beforeRow callback row - %d", rowNum); + LOGGER.error(errMsg, ex); + } + } + + + @Override + void afterRowEnd(final int rowNum, final Map rowDataMap) { + // Sanity Checks + if (Objects.isNull(rowDataMap) || rowDataMap.isEmpty()) { + LOGGER.debug("INVALID Row data Passed - Row #{}", rowNum); + return; + } + + // Skip rows before Header ROW and after Last ROW + if (rowNum < headerRowNum || rowNum > lastRowNum) { + return; + } + + // Process Header ROW + if (rowNum == headerRowNum) { + final Map headerCellRefs = this.asHeaderNameToCellRefMap(rowDataMap); + headerCellRefsMap.putAll(headerCellRefs); + return; + } + + // Check for Column Definitions before processing NON-Header ROWs + + // Row As Bean + final Map rowBean = Spreadsheet.rowAsMap(headerCellRefsMap, rowDataMap); + if (Objects.isNull(rowBean)) { + LOGGER.debug("Unable to construct Row data Bean object - Row #{}", rowNum); + return; + } + + // Row Callback + try { + listener.row(rowNum, rowBean); + } catch (Exception ex) { + String errMsg = String.format("Error calling #row callback row - %d, bean - %s", rowNum, rowBean); + LOGGER.error(errMsg, ex); + } + } + + + // Private Methods + // ------------------------------------------------------------------------ + + private Map asHeaderNameToCellRefMap(final Map headerRowData) { + // Sanity checks + if (Objects.isNull(headerRowData) || headerRowData.isEmpty()) { + return new HashMap<>(); + } + + // Get Bean Column definitions + final Map headerCellRefs = new HashMap(); + for (final String colRef : headerRowData.keySet()) { + final Object header = headerRowData.get(colRef); + + final String headerName = Objects.isNull(header) ? "" : String.valueOf(header); + headerCellRefs.put(headerName, colRef); + } + + LOGGER.debug("Header Name to Cell Refs : {}", headerCellRefs); + return headerCellRefs; + } + + +} diff --git a/src/main/java/io/github/millij/poi/ss/handler/RowContentsHandler.java b/src/main/java/io/github/millij/poi/ss/handler/RowContentsHandler.java index db6d9c0..6822d17 100644 --- a/src/main/java/io/github/millij/poi/ss/handler/RowContentsHandler.java +++ b/src/main/java/io/github/millij/poi/ss/handler/RowContentsHandler.java @@ -9,6 +9,7 @@ import io.github.millij.poi.ss.model.Column; import io.github.millij.poi.util.Spreadsheet; +import io.github.millij.poi.util.Strings; public class RowContentsHandler extends AbstractSheetContentsHandler { @@ -114,7 +115,7 @@ private Map asHeaderNameToCellRefMap(final Map h final Object header = headerRowData.get(colRef); final String headerName = Objects.isNull(header) ? "" : String.valueOf(header); - final String normalHeaderName = Spreadsheet.normalize(headerName); + final String normalHeaderName = Strings.normalize(headerName); headerCellRefs.put(normalHeaderName, colRef); } diff --git a/src/main/java/io/github/millij/poi/ss/reader/AbstractSpreadsheetReader.java b/src/main/java/io/github/millij/poi/ss/reader/AbstractSpreadsheetReader.java index fa42bf5..0d1c5e8 100644 --- a/src/main/java/io/github/millij/poi/ss/reader/AbstractSpreadsheetReader.java +++ b/src/main/java/io/github/millij/poi/ss/reader/AbstractSpreadsheetReader.java @@ -2,6 +2,7 @@ import java.io.InputStream; import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,4 +73,34 @@ public List read(final Class beanClz, final InputStream is, final int } + // + // Read to Map + + @Override + public List> read(final InputStream is) throws SpreadsheetReadException { + // Row Collector + final RowBeanCollector> beanCollector = new RowBeanCollector<>(); + + // Read with callback to fill list + this.read(is, beanCollector); + + // Result + final List> beans = beanCollector.getBeans(); + return beans; + } + + @Override + public List> read(final InputStream is, final int sheetNo) throws SpreadsheetReadException { + // Row Collector + final RowBeanCollector> beanCollector = new RowBeanCollector<>(); + + // Read with callback to fill list + this.read(is, sheetNo, beanCollector); + + // Result + final List> beans = beanCollector.getBeans(); + return beans; + } + + } diff --git a/src/main/java/io/github/millij/poi/ss/reader/SpreadsheetReader.java b/src/main/java/io/github/millij/poi/ss/reader/SpreadsheetReader.java index 9e76a8a..62c73e5 100644 --- a/src/main/java/io/github/millij/poi/ss/reader/SpreadsheetReader.java +++ b/src/main/java/io/github/millij/poi/ss/reader/SpreadsheetReader.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.Map; import io.github.millij.poi.SpreadsheetReadException; import io.github.millij.poi.ss.handler.RowListener; @@ -195,5 +196,58 @@ default List read(Class beanClz, File file, int sheetNo) throws Spread } + // + // Read to Map + + /** + * Reads the spreadsheet file as Generic {@link Map} beans. Note that only the requested sheet (sheet numbers are + * indexed from 0) will be read. + * + * @param is {@link InputStream} of the spreadsheet file + * @param listener Custom {@link RowListener} implementation for row data callbacks. + * + * @throws SpreadsheetReadException when the file data is not readable or row data to bean mapping failed. + */ + void read(InputStream is, RowListener> listener) throws SpreadsheetReadException; + + /** + * Reads the spreadsheet file as Generic {@link Map} beans. Note that only the requested sheet (sheet numbers are + * indexed from 0) will be read. + * + * @param is {@link InputStream} of the spreadsheet file + * @param sheetNo index of the Sheet to be read (index starts from 1) + * @param listener Custom {@link RowListener} implementation for row data callbacks. + * + * @throws SpreadsheetReadException when the file data is not readable or row data to bean mapping failed. + */ + void read(InputStream is, int sheetNo, RowListener> listener) throws SpreadsheetReadException; + + + /** + * Reads the spreadsheet file as Generic {@link Map} beans. Note that only the requested sheet (sheet numbers are + * indexed from 0) will be read. + * + * @param is {@link InputStream} of the spreadsheet file + * + * @return a {@link List} of {@link Map} objects + * + * @throws SpreadsheetReadException when the file data is not readable or row data to bean mapping failed. + */ + List> read(InputStream is) throws SpreadsheetReadException; + + /** + * Reads the spreadsheet file as Generic {@link Map} beans. Note that only the requested sheet (sheet numbers are + * indexed from 0) will be read. + * + * @param is {@link InputStream} of the spreadsheet file + * @param sheetNo index of the Sheet to be read (index starts from 1) + * + * @return a {@link List} of {@link Map} objects + * + * @throws SpreadsheetReadException when the file data is not readable or row data to bean mapping failed. + */ + List> read(InputStream is, int sheetNo) throws SpreadsheetReadException; + + } diff --git a/src/main/java/io/github/millij/poi/ss/reader/XlsReader.java b/src/main/java/io/github/millij/poi/ss/reader/XlsReader.java index e9633c2..99775fe 100644 --- a/src/main/java/io/github/millij/poi/ss/reader/XlsReader.java +++ b/src/main/java/io/github/millij/poi/ss/reader/XlsReader.java @@ -22,6 +22,7 @@ import io.github.millij.poi.ss.handler.RowListener; import io.github.millij.poi.ss.model.Column; import io.github.millij.poi.util.Spreadsheet; +import io.github.millij.poi.util.Strings; /** @@ -81,7 +82,6 @@ public void read(final Class beanClz, final InputStream is, final RowList LOGGER.error(errMsg, ex); throw new SpreadsheetReadException(errMsg, ex); } - } @Override @@ -109,14 +109,90 @@ public void read(final Class beanClz, final InputStream is, final int she } + // + // Read to Map + + @Override + public void read(final InputStream is, final RowListener> listener) + throws SpreadsheetReadException { + try { + final HSSFWorkbook wb = new HSSFWorkbook(is); + final int sheetCount = wb.getNumberOfSheets(); + LOGGER.debug("Total no. of sheets found in HSSFWorkbook : #{}", sheetCount); + + // Iterate over sheets + for (int i = 0; i < sheetCount; i++) { + final HSSFSheet sheet = wb.getSheetAt(i); + LOGGER.debug("Processing HSSFSheet at No. : {}", i); + + // Process Sheet + this.processSheet(sheet, listener); + } + + // Close workbook + wb.close(); + } catch (Exception ex) { + String errMsg = String.format("Error reading HSSFSheet, to Map : %s", ex.getMessage()); + LOGGER.error(errMsg, ex); + throw new SpreadsheetReadException(errMsg, ex); + } + } + + @Override + public void read(final InputStream is, final int sheetNo, final RowListener> listener) + throws SpreadsheetReadException { + try { + final HSSFWorkbook wb = new HSSFWorkbook(is); + final HSSFSheet sheet = wb.getSheetAt(sheetNo - 1); // subtract 1 as Workbook follows 0-based index + + // Process Sheet + this.processSheet(sheet, listener); + + // Close workbook + wb.close(); + } catch (Exception ex) { + String errMsg = String.format("Error reading sheet %d, to Map : %s", sheetNo, ex.getMessage()); + LOGGER.error(errMsg, ex); + throw new SpreadsheetReadException(errMsg, ex); + } + } + + // // Protected Methods // ------------------------------------------------------------------------ + protected void processSheet(final HSSFSheet sheet, final RowListener> eventHandler) { + // Header column - name mapping + final HSSFRow headerRowObj = sheet.getRow(headerRowIdx); + final Map headerCellRefsMap = this.asHeaderNameToCellRefMap(headerRowObj, false); + + final Iterator rows = sheet.rowIterator(); + while (rows.hasNext()) { + // Process Row Data + final HSSFRow row = (HSSFRow) rows.next(); + final int rowNum = row.getRowNum(); + + // Skip rows before Header ROW and after Last ROW + if (rowNum < headerRowIdx || rowNum > lastRowIdx) { + continue; + } + + final Map rowDataMap = this.extractRowDataAsMap(row); + if (rowDataMap == null || rowDataMap.isEmpty()) { + continue; + } + + // Row data as Bean + final Map rowBean = Spreadsheet.rowAsMap(headerCellRefsMap, rowDataMap); + eventHandler.row(rowNum, rowBean); + } + } + protected void processSheet(final Class beanClz, final HSSFSheet sheet, final RowListener eventHandler) { // Header column - name mapping final HSSFRow headerRowObj = sheet.getRow(headerRowIdx); - final Map headerCellRefsMap = this.asHeaderNameToCellRefMap(headerRowObj); + final Map headerCellRefsMap = this.asHeaderNameToCellRefMap(headerRowObj, true); // Bean Properties - column name mapping final Map propColumnMap = Spreadsheet.getPropertyToColumnDefMap(beanClz); @@ -147,7 +223,7 @@ protected void processSheet(final Class beanClz, final HSSFSheet sheet, f // Private Methods // ------------------------------------------------------------------------ - private Map asHeaderNameToCellRefMap(final HSSFRow headerRow) { + private Map asHeaderNameToCellRefMap(final HSSFRow headerRow, final boolean normalizeHeaderName) { // Sanity checks if (Objects.isNull(headerRow)) { return new HashMap<>(); @@ -164,9 +240,9 @@ private Map asHeaderNameToCellRefMap(final HSSFRow headerRow) { // Cell Value final Object header = this.getCellValue(cell); - final String headerName = Objects.isNull(header) ? "" : String.valueOf(header); - final String normalHeaderName = Spreadsheet.normalize(headerName); - headerCellRefs.put(normalHeaderName, cellColRef); + final String rawHeaderName = Objects.isNull(header) ? "" : String.valueOf(header); + final String headerName = normalizeHeaderName ? Strings.normalize(rawHeaderName) : rawHeaderName; + headerCellRefs.put(headerName, cellColRef); } return headerCellRefs; diff --git a/src/main/java/io/github/millij/poi/ss/reader/XlsxReader.java b/src/main/java/io/github/millij/poi/ss/reader/XlsxReader.java index 761b07c..f043e1f 100644 --- a/src/main/java/io/github/millij/poi/ss/reader/XlsxReader.java +++ b/src/main/java/io/github/millij/poi/ss/reader/XlsxReader.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Map; import javax.xml.parsers.ParserConfigurationException; @@ -23,6 +24,7 @@ import org.xml.sax.XMLReader; import io.github.millij.poi.SpreadsheetReadException; +import io.github.millij.poi.ss.handler.RowContentsAsMapHandler; import io.github.millij.poi.ss.handler.RowContentsHandler; import io.github.millij.poi.ss.handler.RowListener; import io.github.millij.poi.util.Beans; @@ -133,6 +135,78 @@ public void read(final Class beanClz, final InputStream is, final int she } + // + // Read to Map + + @Override + public void read(final InputStream is, final RowListener> listener) + throws SpreadsheetReadException { + + // Read + try (final OPCPackage opcPkg = OPCPackage.open(is)) { + // XSSF Reader + final XSSFReader xssfReader = new XSSFReader(opcPkg); + + // XML Reader + final XMLReader xmlReader = this.newXMLReaderInstance(opcPkg, xssfReader, listener); + + // Iterate over sheets + for (SheetIterator worksheets = (SheetIterator) xssfReader.getSheetsData(); worksheets.hasNext();) { + final InputStream sheetInpStream = worksheets.next(); + final String sheetName = worksheets.getSheetName(); + LOGGER.debug("Reading XLSX Sheet :: ", sheetName); + + // Parse sheet + xmlReader.parse(new InputSource(sheetInpStream)); + sheetInpStream.close(); + } + + } catch (Exception ex) { + String errMsg = String.format("Error reading sheet data, to Map : %s", ex.getMessage()); + LOGGER.error(errMsg, ex); + throw new SpreadsheetReadException(errMsg, ex); + } + + } + + @Override + public void read(final InputStream is, final int sheetNo, final RowListener> listener) + throws SpreadsheetReadException { + try (final OPCPackage opcPkg = OPCPackage.open(is)) { + // XSSF Reader + final XSSFReader xssfReader = new XSSFReader(opcPkg); + + // XML Reader + final XMLReader xmlReader = this.newXMLReaderInstance(opcPkg, xssfReader, listener); + + // Iterate over sheets + final SheetIterator worksheets = (SheetIterator) xssfReader.getSheetsData(); + for (int i = 1; worksheets.hasNext(); i++) { + // Get Sheet + final InputStream sheetInpStream = worksheets.next(); + final String sheetName = worksheets.getSheetName(); + + // Check for Sheet No. + if (i != sheetNo) { + continue; + } + + LOGGER.debug("Reading XLSX Sheet :: #{} - {}", i, sheetName); + + // Parse Sheet + xmlReader.parse(new InputSource(sheetInpStream)); + sheetInpStream.close(); + } + + } catch (Exception ex) { + String errMsg = String.format("Error reading sheet %d, to Map : %s", sheetNo, ex.getMessage()); + LOGGER.error(errMsg, ex); + throw new SpreadsheetReadException(errMsg, ex); + } + + } + + // Private Methods // ------------------------------------------------------------------------ @@ -164,4 +238,21 @@ private XMLReader newXMLReaderInstance(final Class beanClz, final OPCPack return xmlReader; } + private XMLReader newXMLReaderInstance(final OPCPackage opcPkg, final XSSFReader xssfReader, + final RowListener> listener) + throws InvalidFormatException, IOException, SAXException, ParserConfigurationException { + // Content Handler + final StylesTable styles = xssfReader.getStylesTable(); + final ReadOnlySharedStringsTable ssTable = new ReadOnlySharedStringsTable(opcPkg); + final SheetContentsHandler sheetHandler = new RowContentsAsMapHandler(listener, headerRowIdx, lastRowIdx); + + final ContentHandler handler = new XSSFSheetXMLHandler(styles, ssTable, sheetHandler, true); + + // XML Reader + final XMLReader xmlReader = XMLHelper.newXMLReader(); + xmlReader.setContentHandler(handler); + + return xmlReader; + } + } diff --git a/src/main/java/io/github/millij/poi/util/Beans.java b/src/main/java/io/github/millij/poi/util/Beans.java index 9cb6984..4884f0d 100644 --- a/src/main/java/io/github/millij/poi/util/Beans.java +++ b/src/main/java/io/github/millij/poi/util/Beans.java @@ -27,7 +27,7 @@ */ public final class Beans { - private static final Logger LOGGER = LoggerFactory.getLogger(Spreadsheet.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Beans.class); private Beans() { super(); @@ -35,6 +35,13 @@ private Beans() { } + // + // Constants + + private static final PropertyUtilsBean PROP_UTILS_BEAN = new PropertyUtilsBean(); + private static final ConvertUtilsBean CONVERT_UTILS_BEAN = new ConvertUtilsBean(); + + // Static Utilities // ------------------------------------------------------------------------ @@ -68,7 +75,7 @@ public static String getFieldName(final Method method) { */ public static String getFieldValueAsString(final Object beanObj, final String fieldName) throws Exception { // Property Descriptor - final PropertyDescriptor pd = new PropertyDescriptor(fieldName, beanObj.getClass()); + final PropertyDescriptor pd = PROP_UTILS_BEAN.getPropertyDescriptor(beanObj, fieldName); final Method getterMtd = pd.getReadMethod(); final Object value = getterMtd.invoke(beanObj); @@ -102,38 +109,106 @@ public static boolean isInstantiableType(final Class clz) { } - // Set Property + // Bean Property :: Get // ------------------------------------------------------------------------ - private static final PropertyUtilsBean PROP_UTILS_BEAN = new PropertyUtilsBean(); - private static final ConvertUtilsBean CONVERT_UTILS_BEAN = new ConvertUtilsBean(); + public static Object getProperty(final Object bean, final String propName) throws Exception { + final Object value = PROP_UTILS_BEAN.getSimpleProperty(bean, propName); + return value; + } + + // Bean Property :: Set + // ------------------------------------------------------------------------ + + /** + */ + public static void setProperty(final Object target, final String propName, final Class propType, + final Object propValue) throws Exception { + // Sanity checks + if (Objects.isNull(propValue)) { + return; // Skip Setter if property value is NULL + } + + try { + // Convert the specified value to the required type + final Object newValue; + if (propValue instanceof String) { + newValue = CONVERT_UTILS_BEAN.convert((String) propValue, propType); + } else { + final Converter converter = CONVERT_UTILS_BEAN.lookup(propType); + if (converter != null) { + newValue = converter.convert(propType, propValue); + } else { + newValue = propValue; + } + } + + // Invoke the setter method + PROP_UTILS_BEAN.setProperty(target, propName, newValue); + + } catch (Exception ex) { + // + } + } + + + /** + */ + @SuppressWarnings({"unchecked", "rawtypes"}) public static void setProperty(final Object target, final String propName, final Object propValue, final String format, final DateTimeType dateTimeType) throws Exception { + // Sanity checks + if (Objects.isNull(propValue)) { + return; // Skip Setter if property value is NULL + } + // Calculate the property type final PropertyDescriptor descriptor = PROP_UTILS_BEAN.getPropertyDescriptor(target, propName); - if (descriptor == null || descriptor.getWriteMethod() == null) { + if (Objects.isNull(descriptor) || Objects.isNull(descriptor.getWriteMethod())) { return; // Skip this property setter } // Property Type - final Class type = descriptor.getPropertyType(); + final Class propType = descriptor.getPropertyType(); - // Check PropValue (expected non-null and string) - if (Objects.isNull(propValue) || !(propValue instanceof String)) { - setProperty(target, propName, type, propValue); + // + // Handle Date/Time/Duration Cases + if (!DateTimeType.NONE.equals(dateTimeType) || propType.equals(Date.class)) { + setDateTimeProperty(target, propName, propType, propValue, format, dateTimeType); return; } // - // Handle Date/Time/Duration Cases + // Handle ENUM + if (propType.isEnum() && (propValue instanceof String)) { + final String cleanEnumStr = Strings.normalize((String) propValue).toUpperCase(); + final Enum enumValue = Enum.valueOf((Class) propType, cleanEnumStr); + setProperty(target, propName, propType, enumValue); + return; + } - // Check if its a Date time property - if (!type.equals(Date.class) && DateTimeType.NONE.equals(dateTimeType)) { - setProperty(target, propName, type, propValue); + // + // Handle Boolean + if (propType.equals(Boolean.class) && (propValue instanceof String)) { + // Cleanup Boolean String + final String cleanBoolStr = Strings.normalize((String) propValue); // for cases like "FALSE()", "TRUE()" + setProperty(target, propName, propType, cleanBoolStr); return; } + // + // Default Handling (for all other Types) + setProperty(target, propName, propType, propValue); + + } + + + /** + * Set the Date/Time property of the Target Bean. + */ + private static void setDateTimeProperty(final Object target, final String propName, final Class propType, + final Object propValue, final String format, final DateTimeType dateTimeType) throws Exception { // Input value Format final String dateFormatStr = Objects.isNull(format) || format.isBlank() ? "dd/MM/yyyy" : format; @@ -141,62 +216,29 @@ public static void setProperty(final Object target, final String propName, final final SimpleDateFormat dateFmt = new SimpleDateFormat(dateFormatStr); final Date dateValue = dateFmt.parse((String) propValue); - // check if the PropType is Date - if (type.equals(Date.class)) { - setProperty(target, propName, type, dateValue); - return; - } - - // Convert to Long - final Long longValue; - if (DateTimeType.DURATION.equals(dateTimeType)) { - final Calendar calendar = Calendar.getInstance(); - calendar.setTime(dateValue); - final long durationMillis = calendar.get(HOUR_OF_DAY) * 3600_000 + // - calendar.get(MINUTE) * 60_000 + // - calendar.get(SECOND) * 1_000; + // Check if the PropType is Date + if (propType.equals(Date.class)) { + setProperty(target, propName, propType, dateValue); - longValue = durationMillis; } else { - longValue = dateValue.getTime(); - } - - setProperty(target, propName, type, longValue); - return; - } - - /** - */ - public static void setProperty(final Object target, final String propName, final Class type, - final Object propValue) throws Exception { - try { - // Convert the specified value to the required type - final Object newValue; - if (propValue instanceof String) { - newValue = CONVERT_UTILS_BEAN.convert((String) propValue, type); + // Convert to Long + final Long longValue; + if (DateTimeType.DURATION.equals(dateTimeType)) { + final Calendar calendar = Calendar.getInstance(); + calendar.setTime(dateValue); + final long durationMillis = calendar.get(HOUR_OF_DAY) * 3600_000 + // + calendar.get(MINUTE) * 60_000 + // + calendar.get(SECOND) * 1_000; + + longValue = durationMillis; } else { - final Converter converter = CONVERT_UTILS_BEAN.lookup(type); - if (converter != null) { - newValue = converter.convert(type, propValue); - } else { - newValue = propValue; - } + longValue = dateValue.getTime(); } - // Invoke the setter method - PROP_UTILS_BEAN.setProperty(target, propName, newValue); - - } catch (Exception ex) { - // + setProperty(target, propName, propType, longValue); } - } - /** - */ - public static Object getProperty(final Object bean, final String propName) throws Exception { - final Object value = PROP_UTILS_BEAN.getSimpleProperty(bean, propName); - return value; + // } - } diff --git a/src/main/java/io/github/millij/poi/util/Spreadsheet.java b/src/main/java/io/github/millij/poi/util/Spreadsheet.java index 2ab8309..3057513 100644 --- a/src/main/java/io/github/millij/poi/util/Spreadsheet.java +++ b/src/main/java/io/github/millij/poi/util/Spreadsheet.java @@ -206,6 +206,37 @@ public static Map asRowDataMap(final Object beanObj, final List< // Write to Bean :: from Row data // ------------------------------------------------------------------------ + public static Map rowAsMap(final Map headerCellRefsMap, + final Map rowDataMap) { + // + try { + // Create new Instance + final Map beanMap = new HashMap(); + + for (final String propColName : headerCellRefsMap.keySet()) { + // Get the Header Cell Ref + final String propCellRef = headerCellRefsMap.get(propColName); + if (Objects.isNull(propCellRef) || propCellRef.isBlank()) { + continue; + } + + // Property Value and Format + final Object propValue = rowDataMap.get(propCellRef); + + // set + beanMap.put(propColName, propValue); + } + + return beanMap; + } catch (Exception ex) { + String errMsg = String.format("Error while creating Row Map, from - %s", rowDataMap); + LOGGER.error(errMsg, ex); + } + + return null; + } + + public static T rowAsBean(Class beanClz, Map propColumnMap, Map headerCellRefsMap, Map rowDataMap) { // Sanity checks @@ -230,7 +261,7 @@ public static T rowAsBean(Class beanClz, Map propColumnMa final String propColName = propColDef.getName(); // Get the Header Cell Ref - final String normalizedColName = Spreadsheet.normalize(propColName); + final String normalizedColName = Strings.normalize(propColName); final String propCellRef = headerCellRefsMap.get(normalizedColName); if (Objects.isNull(propCellRef) || propCellRef.isBlank()) { LOGGER.debug("{} :: No Cell Ref found [Prop - Col] : [{} - {}]", beanClz, propName, propColName); @@ -247,8 +278,9 @@ public static T rowAsBean(Class beanClz, Map propColumnMa // Set the property value in the current row object bean Beans.setProperty(bean, propName, propValue, dataFormat, datetimeType); } catch (Exception ex) { - String errMsg = String.format("Failed to set bean property - %s, value - %s", propName, propValue); - LOGGER.error(errMsg, ex); + String exMsg = ex.getMessage(); + String errMsg = String.format("Error setting prop - %s, val - %s : %s", propName, propValue, exMsg); + LOGGER.error(errMsg); } } @@ -272,7 +304,7 @@ private static boolean validateRowData(final Map rowDataMap, // Prop Column Definition final Column propColDef = propColumnMap.get(propName); final String propColName = propColDef.getName(); - final String normalizedColName = Spreadsheet.normalize(propColName); + final String normalizedColName = Strings.normalize(propColName); // Get the Header Cell Ref final String propCellRef = headerCellRefsMap.containsKey(propColName) // @@ -303,18 +335,12 @@ private static boolean validateRowData(final Map rowDataMap, /** * Normalize the string. typically used for case-insensitive comparison. + * + * @deprecated in favor of {@link Strings#normalize(String)} */ + @Deprecated public static String normalize(final String inStr) { - // Sanity checks - if (Objects.isNull(inStr)) { - return ""; - } - - // Special characters - final String cleanStr = inStr.replaceAll("–", " ").replaceAll("[-\\[\\]/{}:.,;#%=()*+?\\^$|<>&\"\'\\\\]", " "); - final String normalizedStr = cleanStr.toLowerCase().trim().replaceAll("\\s+", "_"); - - return normalizedStr; + return Strings.normalize(inStr); } diff --git a/src/main/java/io/github/millij/poi/util/Strings.java b/src/main/java/io/github/millij/poi/util/Strings.java new file mode 100644 index 0000000..0a8722d --- /dev/null +++ b/src/main/java/io/github/millij/poi/util/Strings.java @@ -0,0 +1,55 @@ +package io.github.millij.poi.util; + +import java.util.Objects; + + +/** + * Odd ball String Utilities + * + * @since 3.1.0 + */ +public final class Strings { + + private Strings() { + super(); + // Utility Class + } + + + // Util Methods + // ------------------------------------------------------------------------ + + /** + * Normalize the string by removing unwanted characters from the String. + * + * @param inStr Input String + * @param replacement Replacement character for the unwanted characters + * + * @return Clean / Normalized String + */ + public static String normalize(final String inStr, final String replacement) { + // Sanity checks + if (Objects.isNull(inStr)) { + return ""; + } + + // Special characters + final String cleanStr = inStr.replaceAll("–", " ").replaceAll("[-\\[\\]/{}:.,;#%=()*+?\\^$|<>&\"\'\\\\]", " "); + final String normalizedStr = cleanStr.toLowerCase().trim().replaceAll("\\s+", replacement); + + return normalizedStr; + } + + /** + * Normalize the string by replacing unwanted characters from the String with "_". + * + * @param inStr Input String + * + * @return Clean / Normalized String + */ + public static String normalize(final String inStr) { + return normalize(inStr, "_"); + } + + +} diff --git a/src/test/java/io/github/millij/bean/DataTypesBean.java b/src/test/java/io/github/millij/bean/DataTypesBean.java new file mode 100644 index 0000000..cb8283d --- /dev/null +++ b/src/test/java/io/github/millij/bean/DataTypesBean.java @@ -0,0 +1,160 @@ +package io.github.millij.bean; + +import static io.github.millij.poi.ss.model.DateTimeType.DATE; +import static io.github.millij.poi.ss.model.DateTimeType.DURATION; + +import java.util.Date; + +import io.github.millij.poi.ss.model.annotations.Sheet; +import io.github.millij.poi.ss.model.annotations.SheetColumn; + + +@Sheet +public class DataTypesBean { + + @SheetColumn("ID") + private String id; + + @SheetColumn("Name") + private String name; + + + // Enum + + @SheetColumn("Gender") + private Gender gender; + + + // Numerical + + @SheetColumn("Age") + private Integer age; + + @SheetColumn("Height") + private Double height; + + + // DateTime fields + + @SheetColumn(value = "Date", datetime = DATE, format = "dd-MM-yyy") + private Date date; + + @SheetColumn(value = "Timestamp", datetime = DATE, format = "dd-MM-yyy HH:mm") + private Long timestamp; // Timestamp + + @SheetColumn(value = "Duration", datetime = DURATION, format = "HH:mm:ss") + private Long duration; + + + // Boolean + + @SheetColumn("Is Active") + private Boolean isActive; + + @SheetColumn("Is Complete") + private Boolean isComplete; + + + // Constructors + // ------------------------------------------------------------------------ + + public DataTypesBean() { + // Default + } + + + // Getters and Setters + // ------------------------------------------------------------------------ + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Gender getGender() { + return gender; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Double getHeight() { + return height; + } + + public void setHeight(Double height) { + this.height = height; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public Boolean getIsComplete() { + return isComplete; + } + + public void setIsComplete(Boolean isComplete) { + this.isComplete = isComplete; + } + + + // Object Methods + // ------------------------------------------------------------------------ + + @Override + public String toString() { + return "DataTypesBean [id=" + id + ", name=" + name + ", gender=" + gender + ", age=" + age + ", height=" + + height + ", date=" + date + ", timestamp=" + timestamp + ", duration=" + duration + ", isActive=" + + isActive + ", isComplete=" + isComplete + "]"; + } + +} diff --git a/src/test/java/io/github/millij/poi/ss/reader/XlsReaderTest.java b/src/test/java/io/github/millij/poi/ss/reader/XlsReaderTest.java index fd1917c..b143957 100644 --- a/src/test/java/io/github/millij/poi/ss/reader/XlsReaderTest.java +++ b/src/test/java/io/github/millij/poi/ss/reader/XlsReaderTest.java @@ -7,6 +7,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.After; import org.junit.Assert; @@ -169,6 +170,7 @@ public void row(int rowNum, Employee employee) { employees.add(employee); LOGGER.info("test_read_xls_single_sheet_with_callback :: Output - {}", employee); + // ? } }); @@ -177,4 +179,28 @@ public void row(int rowNum, Employee employee) { Assert.assertTrue(employees.size() > 0); } + + // Read to Map + + @Test + public void test_read_xlsx_as_Map() throws SpreadsheetReadException, FileNotFoundException { + // Excel Reader + LOGGER.info("test_read_xlsx_as_Map :: Reading file - {}", _filepath_xls_single_sheet); + + // File + final File xlsxFile = new File(_filepath_xls_single_sheet); + + // Reader + final XlsReader reader = new XlsReader(); + + List> employees = reader.read(new FileInputStream(xlsxFile), 1); + Assert.assertNotNull(employees); + Assert.assertTrue(employees.size() > 0); + + for (Map emp : employees) { + LOGGER.info("test_read_xlsx_as_Map :: Output - {}", emp); + } + + } + } diff --git a/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderDataTypesTest.java b/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderDataTypesTest.java new file mode 100644 index 0000000..a870779 --- /dev/null +++ b/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderDataTypesTest.java @@ -0,0 +1,64 @@ +package io.github.millij.poi.ss.reader; + +import java.io.File; +import java.text.ParseException; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.millij.bean.DataTypesBean; +import io.github.millij.poi.SpreadsheetReadException; + + +public class XlsxReaderDataTypesTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(XlsxReaderDataTypesTest.class); + + // XLSX + private String _filepath_xlsx_data_types; + + + // Setup + // ------------------------------------------------------------------------ + + @Before + public void setup() throws ParseException { + // sample files + _filepath_xlsx_data_types = "src/test/resources/sample-files/xlsx_sample_data_types.xlsx"; + } + + @After + public void teardown() { + // nothing to do + } + + + // Tests + // ------------------------------------------------------------------------ + + + // Read from file + + @Test + public void test_read_xlsx_data_types() throws SpreadsheetReadException { + // Excel Reader + LOGGER.info("test_read_xlsx_data_types :: Reading file - {}", _filepath_xlsx_data_types); + XlsxReader reader = new XlsxReader(); + + // Read + List beans = reader.read(DataTypesBean.class, new File(_filepath_xlsx_data_types)); + Assert.assertNotNull(beans); + Assert.assertTrue(beans.size() > 0); + + for (final DataTypesBean bean : beans) { + LOGGER.info("test_read_xlsx_data_types :: Output - {}", bean); + } + } + + +} diff --git a/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderTest.java b/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderTest.java index cf87a36..e6e6c18 100644 --- a/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderTest.java +++ b/src/test/java/io/github/millij/poi/ss/reader/XlsxReaderTest.java @@ -7,11 +7,11 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -181,25 +181,25 @@ public void row(int rowNum, Employee employee) { // Read to Map - - @Ignore @Test - public void test_read_xlsx_as_Map() throws FileNotFoundException { + public void test_read_xlsx_as_Map() throws SpreadsheetReadException, FileNotFoundException { // Excel Reader LOGGER.info("test_read_xlsx_as_Map :: Reading file - {}", _filepath_xlsx_single_sheet); - /* + // File + final File xlsxFile = new File(_filepath_xlsx_single_sheet); + // Reader final XlsxReader reader = new XlsxReader(); - List> employees = reader.readAsMap(new File(_filepath_xlsx_single_sheet), 1); + List> employees = reader.read(new FileInputStream(xlsxFile), 1); Assert.assertNotNull(employees); Assert.assertTrue(employees.size() > 0); for (Map emp : employees) { LOGGER.info("test_read_xlsx_single_sheet :: Output - {}", emp); } - */ + } diff --git a/src/test/resources/log4j2.properties b/src/test/resources/log4j2.properties new file mode 100644 index 0000000..012ed7c --- /dev/null +++ b/src/test/resources/log4j2.properties @@ -0,0 +1,15 @@ +# Properties + +# Root Logger Config +rootLogger.level=INFO +rootLogger.appenderRefs=console +rootLogger.appenderRef.console.ref=STDOUT + +# Appenders Config +appenders=console + +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n + diff --git a/src/test/resources/sample-files/xlsx_sample_data_types.xlsx b/src/test/resources/sample-files/xlsx_sample_data_types.xlsx new file mode 100644 index 0000000..f04c302 Binary files /dev/null and b/src/test/resources/sample-files/xlsx_sample_data_types.xlsx differ diff --git a/src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx b/src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx index 9b00f63..970e148 100644 Binary files a/src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx and b/src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx differ