├── _config.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ ├── resources │ │ ├── sample-files │ │ │ ├── competition_data.xlsx │ │ │ ├── xls_sample_single_sheet.xls │ │ │ ├── xls_sample_multiple_sheets.xls │ │ │ ├── xlsx_sample_single_sheet.xlsx │ │ │ └── xlsx_sample_multiple_sheets.xlsx │ │ └── log4j.properties │ └── java │ │ └── io │ │ └── github │ │ └── millij │ │ ├── bean │ │ ├── Gender.java │ │ ├── Company.java │ │ ├── Employee.java │ │ └── CompetitionData.java │ │ └── poi │ │ └── ss │ │ ├── writer │ │ └── SpreadsheetWriterTest.java │ │ └── reader │ │ ├── XlsReaderTest.java │ │ └── XlsxReaderTest.java └── main │ └── java │ └── io │ └── github │ └── millij │ └── poi │ ├── ColumnDuplicateException.java │ ├── UnsupportedException.java │ ├── ss │ ├── handler │ │ ├── RowListener.java │ │ ├── AbstractSheetContentsHandler.java │ │ └── RowContentsHandler.java │ ├── model │ │ ├── annotations │ │ │ ├── Sheet.java │ │ │ └── SheetColumn.java │ │ ├── SheetRow.java │ │ └── ColumnMapping.java │ ├── reader │ │ ├── XlsxReader.java │ │ ├── AbstractSpreadsheetReader.java │ │ ├── XlsReader.java │ │ └── SpreadsheetReader.java │ └── writer │ │ └── SpreadsheetWriter.java │ ├── CellEmptyException.java │ ├── SpreadsheetReadException.java │ ├── ColumnNotFoundException.java │ ├── CellException.java │ └── util │ ├── Beans.java │ └── Spreadsheet.java ├── .travis.yml ├── LICENSE ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/sample-files/competition_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/src/test/resources/sample-files/competition_data.xlsx -------------------------------------------------------------------------------- /src/test/java/io/github/millij/bean/Gender.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.bean; 2 | 3 | 4 | public enum Gender { 5 | 6 | MALE, 7 | 8 | FEMALE; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/sample-files/xls_sample_single_sheet.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/src/test/resources/sample-files/xls_sample_single_sheet.xls -------------------------------------------------------------------------------- /src/test/resources/sample-files/xls_sample_multiple_sheets.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/src/test/resources/sample-files/xls_sample_multiple_sheets.xls -------------------------------------------------------------------------------- /src/test/resources/sample-files/xlsx_sample_single_sheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/src/test/resources/sample-files/xlsx_sample_single_sheet.xlsx -------------------------------------------------------------------------------- /src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funpad/excel-object-mapper/HEAD/src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 11 00:30:31 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip 7 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n 9 | 10 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/bean/Company.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.bean; 2 | 3 | import io.github.millij.poi.ss.model.annotations.Sheet; 4 | import io.github.millij.poi.ss.model.annotations.SheetColumn; 5 | import lombok.*; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | @Sheet("Companies") 11 | public class Company { 12 | 13 | @SheetColumn("Company Name") 14 | private String name; 15 | 16 | @SheetColumn("# of Employees") 17 | private Integer noOfEmployees; 18 | 19 | @SheetColumn("Address") 20 | private String address; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ColumnDuplicateException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | /** 4 | * When the sheet column/header is duplicate, this exception will be thrown. 5 | * 6 | * @author Fang Gang 7 | */ 8 | public class ColumnDuplicateException extends CellException { 9 | private static final long serialVersionUID = 1L; 10 | 11 | public ColumnDuplicateException(String cellReference, String cellColumnName) { 12 | super(cellReference, cellColumnName, 13 | String.format("Cell[%s] :: refuse duplicate header - %s.", cellReference, cellColumnName)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/UnsupportedException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | /** 4 | * Unsupport exception 5 | * 6 | * @author milli 7 | */ 8 | public class UnsupportedException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 3103542175797043236L; 11 | 12 | 13 | // Constructors 14 | // ------------------------------------------------------------------------ 15 | 16 | public UnsupportedException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public UnsupportedException(String message) { 21 | super(message); 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/handler/RowListener.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.handler; 2 | 3 | import io.github.millij.poi.ss.reader.SpreadsheetReader; 4 | 5 | 6 | /** 7 | * An abstract representation of Row level Callback for {@link SpreadsheetReader} implementations. 8 | */ 9 | public interface RowListener { 10 | 11 | /** 12 | * This method will be called after every row by the {@link SpreadsheetReader} implementation. 13 | * 14 | * @param rowNum the Row Number in the sheet. (indexed from 0) 15 | * @param rowObj the java bean constructed using the Row data. 16 | */ 17 | void row(int rowNum, T rowObj); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/CellEmptyException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | /** 4 | * When a cell's column is marked as non-empty by @SheetColumn, the cell is empty, 5 | * and then this exception will be thrown.
6 | *

7 | * See {@link io.github.millij.poi.ss.model.annotations.SheetColumn} 8 | * 9 | * @author Fang Gang 10 | */ 11 | public class CellEmptyException extends CellException { 12 | private static final long serialVersionUID = 1L; 13 | 14 | public CellEmptyException(String cellReference, String cellColumnName) { 15 | super(cellReference, cellColumnName, 16 | String.format("Cell[%s] :: The '%s' not allow empty.", cellReference, cellColumnName)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | env: 4 | - CODECOV_TOKEN="6f3f7d4d-3eab-4605-b631-294c4fb7a58f" 5 | # - COVERALLS_REPO_TOKEN="4CaLAImlPNsl2mLKmrZZNkb59GR2hROro" 6 | 7 | cache: 8 | directories: 9 | - $HOME/.gradle/caches/ 10 | - $HOME/.gradle/wrapper/ 11 | - $HOME/.gradle/nodejs/ 12 | - node_modules 13 | 14 | 15 | before_install: 16 | - chmod +x gradlew 17 | 18 | install: 19 | - ./gradlew clean 20 | 21 | 22 | script: 23 | - ./gradlew check build -x signArchives -x uploadArchives 24 | 25 | 26 | before_cache: 27 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 28 | 29 | 30 | after_success: 31 | - ./gradlew jacocoTestReport 32 | - bash <(curl -s https://codecov.io/bash) 33 | # - ./gradlew coveralls 34 | 35 | after_failure: 36 | - ./gradlew clean check --debug 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/SpreadsheetReadException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | 4 | import lombok.Getter; 5 | 6 | /** 7 | * Spread sheet read exception. 8 | * 9 | * @author Fang Gang 10 | */ 11 | public class SpreadsheetReadException extends Exception { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | @Getter 16 | private String sheetName; 17 | 18 | // Constructors 19 | // ------------------------------------------------------------------------ 20 | 21 | public SpreadsheetReadException(String sheetName, Throwable cause) { 22 | super(formatMessage(sheetName, cause), cause); 23 | this.sheetName = sheetName; 24 | } 25 | 26 | private static String formatMessage(String sheetName, Throwable cause) { 27 | if (sheetName == null) { 28 | sheetName = "?"; 29 | } 30 | return "Sheet[" + sheetName + "] " + cause.getMessage(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ColumnNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * When the sheet column/header is empty, this exception will be thrown. 7 | * 8 | * @author Fang Gang 9 | */ 10 | public class ColumnNotFoundException extends RuntimeException { 11 | private static final long serialVersionUID = 1L; 12 | 13 | /** 14 | * The row number of columns/headers. 15 | */ 16 | @Getter 17 | private int rowNum; 18 | 19 | @Getter 20 | private String cellColumnName; 21 | 22 | public ColumnNotFoundException(int rowNum) { 23 | super(String.format(":: Missing required column in row #%d", rowNum + 1)); 24 | this.rowNum = rowNum; 25 | } 26 | 27 | public ColumnNotFoundException(int rowNum, String cellColumnName) { 28 | super(String.format(":: Missing required column(%s) in row #%d", cellColumnName, rowNum + 1)); 29 | this.rowNum = rowNum; 30 | this.cellColumnName = cellColumnName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/CellException.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi; 2 | 3 | import lombok.Getter; 4 | 5 | 6 | /** 7 | * Base cell exception. 8 | * 9 | * @author Fang Gang 10 | */ 11 | public class CellException extends RuntimeException { 12 | private static final long serialVersionUID = 1L; 13 | 14 | /** 15 | * The reference of cell. 16 | */ 17 | @Getter 18 | private String cellReference; 19 | 20 | /** 21 | * The header/column name of cell. 22 | */ 23 | @Getter 24 | private String cellColumnName; 25 | 26 | public CellException(String cellReference, String cellColumnName, String message) { 27 | super(message); 28 | this.cellReference = cellReference; 29 | this.cellColumnName = cellColumnName; 30 | } 31 | 32 | public CellException(String cellReference, String cellColumnName) { 33 | super(String.format("Cell[%s] :: error, column: %s.", cellReference, cellColumnName)); 34 | this.cellReference = cellReference; 35 | this.cellColumnName = cellColumnName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 millij 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/bean/Employee.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.bean; 2 | 3 | import io.github.millij.poi.ss.model.annotations.Sheet; 4 | import io.github.millij.poi.ss.model.annotations.SheetColumn; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.ToString; 8 | 9 | @Getter 10 | @Setter 11 | @ToString 12 | @Sheet 13 | public class Employee { 14 | 15 | // Note that Id and Name are annotated at name level 16 | @SheetColumn(value = "ID", nullable = false) 17 | private String id; 18 | @SheetColumn(value = "Name") 19 | private String name; 20 | 21 | @SheetColumn("Age") 22 | private Integer age; 23 | 24 | @SheetColumn("Gender") 25 | private String gender; 26 | 27 | @SheetColumn("Height (mts)") 28 | private Double height; 29 | 30 | @SheetColumn("Address") 31 | private String address; 32 | 33 | 34 | // Constructors 35 | // ------------------------------------------------------------------------ 36 | 37 | public Employee() { 38 | // Default 39 | } 40 | 41 | public Employee(String id, String name, Integer age, String gender, Double height) { 42 | super(); 43 | 44 | this.id = id; 45 | this.name = name; 46 | this.age = age; 47 | this.gender = gender; 48 | this.height = height; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/bean/CompetitionData.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.bean; 2 | 3 | import io.github.millij.poi.ss.model.annotations.Sheet; 4 | import io.github.millij.poi.ss.model.annotations.SheetColumn; 5 | import lombok.Data; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * @author Fang Gang 11 | * @date 2019/10/17 12 | **/ 13 | @Data 14 | @Sheet 15 | public class CompetitionData { 16 | 17 | private Long id; 18 | 19 | private Long competitionId; 20 | 21 | @SheetColumn(value = "赛项") 22 | private String competitionType; 23 | 24 | @SheetColumn(value = "学员编号", exclusive = true) 25 | private String studentNo; 26 | 27 | @SheetColumn(value = "用户ID", nullable = false, exclusive = true) 28 | private String userId; 29 | 30 | @SheetColumn(value = "学员姓名", nullable = false) 31 | private String studentName; 32 | 33 | @SheetColumn(value = "总分", nullable = false) 34 | private Double score; 35 | 36 | @SheetColumn(value = "奖项", nullable = false) 37 | private String award; 38 | 39 | @SheetColumn(value = "城市", nullable = false) 40 | private String city; 41 | 42 | @SheetColumn(value = "证书编号", nullable = false) 43 | private String certificateNo; 44 | 45 | @SheetColumn 46 | private Date createTime; 47 | 48 | private String createUser; 49 | 50 | private Date modifyTime; 51 | 52 | private String modifyUser; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/model/annotations/Sheet.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.model.annotations; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | 10 | /** 11 | * Marker annotation that can be used to define a "type" for a sheet. The value of this annotation 12 | * will be used to map the sheet of the workbook to this bean definition. 13 | * 14 | *

15 | * Default value ("") indicates that the default sheet name to be used without any modifications, 16 | * but it can be specified to non-empty value to specify different name. 17 | *

18 | * 19 | *

20 | * Typically used when writing the java objects to the file. 21 | *

22 | * 23 | * @author milli, Fang Gang 24 | */ 25 | @Retention(RUNTIME) 26 | @Target(TYPE) 27 | public @interface Sheet { 28 | 29 | /** 30 | * Name of the sheet. 31 | * 32 | * @return the Sheet name 33 | */ 34 | String value() default ""; 35 | 36 | /** 37 | * Index of the sheet, start with 1. 38 | * 39 | * @return the sheet index 40 | */ 41 | int index() default 1; 42 | 43 | /** 44 | * Row number of the header, start with 1. 45 | * 46 | * @return the row number of the header 47 | */ 48 | int headerAtRow() default 1; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/model/annotations/SheetColumn.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.model.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | 9 | /** 10 | * Marker annotation that can be used to define a non-static method as a "setter" or "getter" for a 11 | * column, or non-static field to be used as a column. 12 | * 13 | *

14 | * Default value ("") indicates that the field name is used as the column name without any 15 | * modifications, but it can be specified to non-empty value to specify different name. 16 | *

17 | * 18 | * @author milli, Fang Gang 19 | */ 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Target({ElementType.FIELD}) 22 | public @interface SheetColumn { 23 | 24 | /** 25 | * Name of the column to map the annotated property with. 26 | * 27 | * @return column name/header. 28 | */ 29 | String value() default ""; 30 | 31 | /** 32 | * Setting this to false will enable the null check on the Column values, to ensure 33 | * non-null values for the field. 34 | *

35 | * default is true. i.e., null values are allowed. 36 | * 37 | * @return true if the annotated field is allowed null as value. 38 | */ 39 | boolean nullable() default true; 40 | 41 | boolean exclusive() default false; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/handler/AbstractSheetContentsHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.handler; 2 | 3 | import io.github.millij.poi.ss.model.SheetRow; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler; 7 | import org.apache.poi.xssf.usermodel.XSSFComment; 8 | 9 | /** 10 | * @author milli, Fang Gang 11 | */ 12 | @Slf4j 13 | abstract class AbstractSheetContentsHandler implements SheetContentsHandler { 14 | 15 | private SheetRow sheetRow; 16 | 17 | // Methods 18 | // ------------------------------------------------------------------------ 19 | 20 | // Abstract 21 | 22 | abstract void beforeRowStart(int rowNum); 23 | 24 | abstract void afterRowEnd(final SheetRow sheetRow); 25 | 26 | 27 | 28 | // SheetContentsHandler Implementations 29 | // ------------------------------------------------------------------------ 30 | 31 | @Override 32 | public void startRow(int rowNum) { 33 | // Callback 34 | this.beforeRowStart(rowNum); 35 | 36 | // Start handle row 37 | this.sheetRow = new SheetRow(rowNum); 38 | } 39 | 40 | @Override 41 | public void endRow(int rowNum) { 42 | // Callback 43 | this.afterRowEnd(sheetRow); 44 | } 45 | 46 | @Override 47 | public void cell(String cellRef, String cellVal, XSSFComment comment) { 48 | // Sanity Checks 49 | if (StringUtils.isEmpty(cellRef)) { 50 | log.error("Row[#] {} : Cell reference is empty - {}", sheetRow.getRowNum(), cellRef); 51 | return; 52 | } 53 | 54 | if (StringUtils.isEmpty(cellVal)) { 55 | log.warn("Row[#] {} - Cell[ref] formatted value is empty : {} - {}", 56 | sheetRow.getRowNum(), cellRef, cellVal); 57 | return; 58 | } 59 | 60 | // Set the CellValue into the SheetRow 61 | sheetRow.addCell(cellRef, cellVal); 62 | log.debug("cell - Saving Column value : {} - {}", cellRef, cellVal); 63 | } 64 | 65 | @Override 66 | public void headerFooter(String text, boolean isHeader, String tagName) { 67 | // TODO Auto-generated method stub 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Gradle ### 4 | .gradle 5 | gradle.properties 6 | build/ 7 | 8 | # Ignore Gradle GUI config 9 | gradle-app.setting 10 | 11 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 12 | !gradle-wrapper.jar 13 | 14 | 15 | ### Intellij ### 16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 17 | *.iml 18 | .idea/ 19 | 20 | ## Directory-based project format: 21 | external-modules/ 22 | generated/ 23 | 24 | # if you remove the above rule, at least ignore the following: 25 | 26 | # User-specific stuff: 27 | # .idea/workspace.xml 28 | # .idea/tasks.xml 29 | # .idea/dictionaries 30 | 31 | # Sensitive or high-churn files: 32 | # .idea/dataSources.ids 33 | # .idea/dataSources.xml 34 | # .idea/sqlDataSources.xml 35 | # .idea/dynamic.xml 36 | # .idea/uiDesigner.xml 37 | 38 | # Gradle: 39 | # .idea/gradle.xml 40 | # .idea/libraries 41 | 42 | # Mongo Explorer plugin: 43 | # .idea/mongoSettings.xml 44 | 45 | ## File-based project format: 46 | *.ipr 47 | *.iws 48 | 49 | ## Plugin-specific files: 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Crashlytics plugin (for Android Studio and IntelliJ) 61 | com_crashlytics_export_strings.xml 62 | crashlytics.properties 63 | crashlytics-build.properties 64 | 65 | 66 | ### Eclipse ### 67 | *.pydevproject 68 | .metadata 69 | .gradle 70 | tmp/ 71 | *.tmp 72 | *.bak 73 | *.swp 74 | *~.nib 75 | local.properties 76 | .settings/ 77 | .loadpath 78 | 79 | # Eclipse Core 80 | .project 81 | 82 | # External tool builders 83 | .externalToolBuilders/ 84 | 85 | # Locally stored "Eclipse launch configurations" 86 | *.launch 87 | 88 | # CDT-specific 89 | .cproject 90 | 91 | # JDT-specific (Eclipse Java Development Tools) 92 | .classpath 93 | 94 | # PDT-specific 95 | .buildpath 96 | 97 | # sbteclipse plugin 98 | .target 99 | 100 | # TeXlipse plugin 101 | .texlipse 102 | 103 | 104 | ### Java ### 105 | *.class 106 | 107 | # Mobile Tools for Java (J2ME) 108 | .mtj.tmp/ 109 | 110 | # Package Files # 111 | *.jar 112 | *.war 113 | *.ear 114 | *~ 115 | 116 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 117 | hs_err_pid* 118 | git_version 119 | /bin 120 | 121 | ### JUnit : Tests ## 122 | test-cases/ 123 | test-output/ 124 | 125 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/handler/RowContentsHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.handler; 2 | 3 | import io.github.millij.poi.ss.model.ColumnMapping; 4 | import io.github.millij.poi.ss.model.SheetRow; 5 | import io.github.millij.poi.util.Spreadsheet; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | /** 9 | * Row contents handler. 10 | * 11 | * @param Class Type 12 | * @author milli, Fang Gang 13 | */ 14 | @Slf4j 15 | public class RowContentsHandler extends AbstractSheetContentsHandler { 16 | 17 | private final Class beanClz; 18 | 19 | private final int headerRow; 20 | 21 | private ColumnMapping columnMapping; 22 | 23 | private final RowListener rowListener; 24 | 25 | 26 | // Constructors 27 | // ------------------------------------------------------------------------ 28 | 29 | public RowContentsHandler(Class beanClz, RowListener rowListener) { 30 | this(beanClz, rowListener, 0); 31 | } 32 | 33 | public RowContentsHandler(Class beanClz, RowListener rowListener, int headerRow) { 34 | super(); 35 | 36 | this.beanClz = beanClz; 37 | this.headerRow = headerRow; 38 | this.rowListener = rowListener; 39 | } 40 | 41 | 42 | // AbstractSheetContentsHandler Methods 43 | // ------------------------------------------------------------------------ 44 | 45 | @Override 46 | void beforeRowStart(int rowNum) { 47 | log.debug("Start reading row - {}.", rowNum); 48 | } 49 | 50 | 51 | @Override 52 | void afterRowEnd(final SheetRow sheetRow) { 53 | // Sanity Checks 54 | if (sheetRow == null || sheetRow.isEmpty()) { 55 | return; 56 | } 57 | 58 | final int rowNum = sheetRow.getRowNum(); 59 | 60 | // Skip rows before header row 61 | if (rowNum < headerRow) { 62 | return; 63 | } 64 | 65 | if (rowNum == headerRow) { 66 | columnMapping = new ColumnMapping(beanClz, sheetRow); 67 | return; 68 | } 69 | 70 | // Row As Bean 71 | T rowBean = Spreadsheet.rowAsBean(sheetRow, columnMapping); 72 | 73 | // Row Callback 74 | try { 75 | rowListener.row(rowNum, rowBean); 76 | } catch (Exception ex) { 77 | String errMsg = String.format("Error calling listener callback row - %d, bean - %s", rowNum, rowBean); 78 | throw new RuntimeException(errMsg, ex); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/util/Beans.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.util; 2 | 3 | import java.beans.Introspector; 4 | import java.beans.PropertyDescriptor; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Modifier; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | /** 13 | * Added Bean Utilities that are not directly available in the Apache BeanUtils library. 14 | * @author milli 15 | */ 16 | public final class Beans { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(Spreadsheet.class); 19 | 20 | private Beans() { 21 | // Utility Class 22 | } 23 | 24 | 25 | // Static Utilities 26 | // ------------------------------------------------------------------------ 27 | 28 | /** 29 | * Extrats the name of the field from its accessor method. 30 | * 31 | * @param method any accessor {@link Method} of the field. 32 | * @return the name of the field. 33 | */ 34 | public static String getFieldName(Method method) { 35 | String methodName = method.getName(); 36 | return Introspector.decapitalize(methodName.substring(methodName.startsWith("is") ? 2 : 3)); 37 | } 38 | 39 | 40 | /** 41 | * Given a Bean and a field of it, returns the value of the field converted to String. 42 | * 43 | *

47 | * 48 | * @param beanObj bean of which the field value to be extracted. 49 | * @param fieldName Name of the property/field of the object. 50 | * @return the field value converted to String. 51 | * 52 | * @throws Exception if the bean or the fields accessor methods are not accessible. 53 | */ 54 | public static String getFieldValueAsString(Object beanObj, String fieldName) throws Exception { 55 | // Property Descriptor 56 | PropertyDescriptor pd = new PropertyDescriptor(fieldName, beanObj.getClass()); 57 | Method getterMtd = pd.getReadMethod(); 58 | 59 | Object value = getterMtd.invoke(beanObj); 60 | String cellValue = value != null ? String.valueOf(value) : null; 61 | 62 | return cellValue; 63 | } 64 | 65 | 66 | /** 67 | * Check whether a class is instantiable of not. 68 | * 69 | * @param clz the {@link Class} which needs to verified. 70 | * @return false if the class in primitive/abstract/interface/array 71 | */ 72 | public static boolean isInstantiableType(Class clz) { 73 | // Sanity checks 74 | if (clz == null) { 75 | return false; 76 | } 77 | 78 | int modifiers = clz.getModifiers(); 79 | LOGGER.debug("Modifiers of Class : {} - {}", clz, modifiers); 80 | 81 | // Primitive / Abstract / Interface / Array 82 | if (clz.isPrimitive() || Modifier.isAbstract(modifiers) || clz.isInterface() || clz.isArray()) { 83 | return false; 84 | } 85 | 86 | return true; 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/reader/XlsxReader.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.poi.SpreadsheetReadException; 4 | import io.github.millij.poi.ss.handler.RowContentsHandler; 5 | import io.github.millij.poi.ss.handler.RowListener; 6 | import lombok.Cleanup; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.poi.openxml4j.opc.OPCPackage; 9 | import org.apache.poi.ss.usermodel.Workbook; 10 | import org.apache.poi.util.XMLHelper; 11 | import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable; 12 | import org.apache.poi.xssf.eventusermodel.XSSFReader; 13 | import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler; 14 | import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler; 15 | import org.apache.poi.xssf.model.StylesTable; 16 | import org.xml.sax.ContentHandler; 17 | import org.xml.sax.InputSource; 18 | import org.xml.sax.XMLReader; 19 | 20 | import java.io.InputStream; 21 | 22 | import static io.github.millij.poi.util.Beans.isInstantiableType; 23 | 24 | 25 | /** 26 | * Reader impletementation of {@link Workbook} for an OOXML .xlsx file. This implementation is 27 | * suitable for low memory sax parsing or similar. 28 | * 29 | * @see XlsReader 30 | */ 31 | @Slf4j 32 | public class XlsxReader extends AbstractSpreadsheetReader { 33 | 34 | // Constructor 35 | 36 | public XlsxReader() { 37 | super(); 38 | } 39 | 40 | 41 | // SpreadsheetReader Impl 42 | // ------------------------------------------------------------------------ 43 | 44 | @Override 45 | public void read(Class beanClz, InputStream is, RowListener listener) 46 | throws SpreadsheetReadException { 47 | readBySheetNo(beanClz, is, null, listener); 48 | } 49 | 50 | 51 | @Override 52 | public void read(Class beanClz, InputStream is, int sheetNo, RowListener listener) 53 | throws SpreadsheetReadException { 54 | readBySheetNo(beanClz, is, sheetNo, listener); 55 | } 56 | 57 | private void readBySheetNo(Class beanClz, InputStream is, Integer sheetNo, RowListener listener) 58 | throws SpreadsheetReadException { 59 | // Sanity checks 60 | if (!isInstantiableType(beanClz)) { 61 | throw new IllegalArgumentException("XlsxReader :: Invalid bean type passed!"); 62 | } 63 | 64 | String sheetName = ""; 65 | 66 | try (final OPCPackage opcPkg = OPCPackage.open(is)) { 67 | // XSSF Reader 68 | XSSFReader xssfReader = new XSSFReader(opcPkg); 69 | 70 | // Content Handler 71 | StylesTable styles = xssfReader.getStylesTable(); 72 | ReadOnlySharedStringsTable ssTable = new ReadOnlySharedStringsTable(opcPkg, false); 73 | SheetContentsHandler sheetHandler = new RowContentsHandler(beanClz, listener, 0); 74 | 75 | ContentHandler handler = new XSSFSheetXMLHandler(styles, ssTable, sheetHandler, true); 76 | 77 | // XML Reader 78 | XMLReader xmlParser = XMLHelper.newXMLReader(); 79 | xmlParser.setContentHandler(handler); 80 | 81 | // Iterate over sheets 82 | XSSFReader.SheetIterator worksheets = (XSSFReader.SheetIterator) xssfReader.getSheetsData(); 83 | for (int i = 0; worksheets.hasNext(); i++) { 84 | @Cleanup InputStream sheetInpStream = worksheets.next(); 85 | 86 | // If sheetNo is specified, skip non-specific sheets. 87 | if (sheetNo != null && sheetNo != i) { 88 | continue; 89 | } 90 | log.info("Reading the XSSFSheet(idx{}): {}.", i, sheetName = worksheets.getSheetName()); 91 | // Parse Sheet 92 | xmlParser.parse(new InputSource(sheetInpStream)); 93 | } 94 | } catch (Exception ex) { 95 | log.error("XSSFSheet to Bean({}) Error - Sheet[{}] {}", beanClz.getSimpleName(), sheetName, ex.getMessage()); 96 | throw new SpreadsheetReadException(sheetName, ex); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/funpad/excel-object-mapper.svg?branch=master)](https://travis-ci.org/funpad/excel-object-mapper) 3 | [![codecov](https://codecov.io/gh/funpad/excel-object-mapper/branch/master/graph/badge.svg)](https://codecov.io/gh/funpad/excel-object-mapper) 4 | 5 | 6 | # excel-object-mapper 7 | 8 | **excel-object-mapper** is a wrapper java library for [Apache POI](https://poi.apache.org/) (Apache POI provides java API to read Microsoft Office Formats). POI APIs are very low level giving acess to all the internals of the file formats. 9 | 10 | The aim of this project is to provide easy to use highlevel APIs to read the Office file formats by wrapping the POI APIs. In simple terms, the wrapper APIs would look similar to the [Jackson Project for XML and JSON](https://github.com/FasterXML/jackson), where the data can be mapped to a JAVA Bean and through the mapper APIs, the file data can directly be read as java objects. 11 | 12 | *- Note that the library supports only **spreadsheets** (Excel files).* 13 | 14 | 15 | ## Include 16 | 17 | This library is available in [Maven Central](https://mvnrepository.com/artifact/io.github.funpad/excel-object-mapper). 18 | 19 | `pom.xml` entry details.. 20 | 21 | ``` 22 | 23 | io.github.funpad 24 | excel-object-mapper 25 | 2.0.0 26 | 27 | ``` 28 | 29 | To install manually, please check the [releases](https://github.com/funpad/excel-object-mapper/releases) page for available versions and change log. 30 | 31 | #### Dependencies 32 | 33 | The current implementation uses **POI version 4.1.2**. 34 | 35 | 36 | ## Usage 37 | 38 | ### Spreadsheets (Excel) 39 | 40 | Consider the below sample spreadsheet, where data of employees is present. 41 | 42 | | Name | Age | Gender | Height (mts) | Address | 43 | | ----------------- |:----- | :----- | ------------:| :--------------------------------- | 44 | | Bob | 32 | MALE | 1.8 | 410, Madison, Seattle, WA – 123456 | 45 | | John Doe | 45 | MALE | 2.1 | | 46 | | Guiliano Carlini | | MALE | 1.78 | Palo Alto, CA – 43234 | 47 | 48 | 49 | ##### Mapping Rows to a Java Bean 50 | 51 | Create a java bean and map its properties to the columns using the `@SheetColumn` annotation. The `@SheetColumn` annotation can be declared on the `Field`, as well as its `Accessor Methods`. Pick any one of them to configure the mapped `Column` as per convenience. 52 | 53 | ```java 54 | @Sheet 55 | public class Employee { 56 | // Pick either field or its accessor methods to apply the Column mapping. 57 | ... 58 | @SheetColumn("Age") 59 | private Integer age; 60 | ... 61 | @SheetColumn("Name") 62 | public String getName() { 63 | return name; 64 | } 65 | ... 66 | } 67 | ``` 68 | 69 | ##### Reading Rows as Java Objects 70 | 71 | Once a mapped Java Bean is ready, use a `Reader` to read the file rows as objects. Use `XlsReader` for `.xls` files and `XlsxReader` for `.xlsx` files. 72 | 73 | Reading spreadsheet rows as objects .. 74 | 75 | ```java 76 | ... 77 | final File xlsxFile = new File(""); 78 | final XlsReader reader = new XlsReader(); 79 | List employees = reader.read(Employee.class, xlsxFile); 80 | ... 81 | ``` 82 | 83 | ##### Writing a collection of objects to file 84 | 85 | *Currently writing to `.xlsx` files only is supported* 86 | 87 | ```java 88 | ... 89 | // Employees 90 | List employees = new ArrayList(); 91 | employees.add(new Employee("1", "foo", 12, "MALE", 1.68)); 92 | employees.add(new Employee("2", "bar", null, "MALE", 1.68)); 93 | employees.add(new Employee("3", "foo bar", null, null, null)); 94 | 95 | // Writer 96 | SpreadsheetWriter writer = new SpreadsheetWriter(""); 97 | writer.addSheet(Employee.class, employees); 98 | writer.write(); 99 | ... 100 | ``` 101 | 102 | ## Implementation Details 103 | 104 | 105 | 106 | ## Issues 107 | 108 | The known issues are already listed under [Issues Section](https://github.com/funpad/excel-object-mapper/issues). 109 | 110 | Please add there your bugs findings, feature requests, enhancements etc. 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/poi/ss/writer/SpreadsheetWriterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.writer; 2 | 3 | import io.github.millij.bean.Company; 4 | import io.github.millij.bean.Employee; 5 | import io.github.millij.poi.ss.writer.SpreadsheetWriter; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.text.ParseException; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | 21 | public class SpreadsheetWriterTest { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(SpreadsheetWriterTest.class); 24 | 25 | private final String _path_test_output = "test-cases/output/"; 26 | 27 | // Setup 28 | // ------------------------------------------------------------------------ 29 | 30 | @Before 31 | public void setup() throws ParseException { 32 | // prepare 33 | File output_dir = new File(_path_test_output); 34 | if (!output_dir.exists()) { 35 | output_dir.mkdirs(); 36 | } 37 | } 38 | 39 | @After 40 | public void teardown() { 41 | // nothing to do 42 | } 43 | 44 | 45 | // Tests 46 | // ------------------------------------------------------------------------ 47 | 48 | @Test 49 | public void test_write_xlsx_single_sheet() throws IOException { 50 | final String filepath_output_file = _path_test_output.concat("single_sheet.xlsx"); 51 | 52 | // Excel Writer 53 | LOGGER.info("test_write_xlsx_single_sheet :: Writing to file - {}", filepath_output_file); 54 | SpreadsheetWriter gew = new SpreadsheetWriter(filepath_output_file); 55 | 56 | // Employees 57 | List employees = new ArrayList(); 58 | employees.add(new Employee("1", "foo", 12, "MALE", 1.68)); 59 | employees.add(new Employee("2", "bar", null, "MALE", 1.68)); 60 | employees.add(new Employee("3", "foo bar", null, null, null)); 61 | 62 | // Write 63 | gew.addSheet(Employee.class, employees); 64 | gew.write(); 65 | } 66 | 67 | @Test 68 | public void test_write_xlsx_single_sheet_custom_headers() throws IOException { 69 | final String filepath_output_file = _path_test_output.concat("single_sheet_custom_headers.xlsx"); 70 | 71 | // Excel Writer 72 | LOGGER.info("test_write_xlsx_single_sheet :: Writing to file - {}", filepath_output_file); 73 | SpreadsheetWriter gew = new SpreadsheetWriter(filepath_output_file); 74 | 75 | // Employees 76 | List employees = new ArrayList(); 77 | employees.add(new Employee("1", "foo", 12, "MALE", 1.68)); 78 | employees.add(new Employee("2", "bar", null, "MALE", 1.68)); 79 | employees.add(new Employee("3", "foo bar", null, null, null)); 80 | 81 | List headers = Arrays.asList("ID", "Age", "Name", "Address"); 82 | 83 | // Add Sheets 84 | gew.addSheet(Employee.class, employees, headers); 85 | 86 | // Write 87 | gew.write(); 88 | } 89 | 90 | 91 | @Test 92 | public void test_write_xlsx_multiple_sheets() throws IOException { 93 | final String filepath_output_file = _path_test_output.concat("multiple_sheets.xlsx"); 94 | 95 | // Excel Writer 96 | LOGGER.info("test_write_xlsx_single_sheet :: Writing to file - {}", filepath_output_file); 97 | SpreadsheetWriter gew = new SpreadsheetWriter(filepath_output_file); 98 | 99 | // Employees 100 | List employees = new ArrayList(); 101 | employees.add(new Employee("1", "foo", 12, "MALE", 1.68)); 102 | employees.add(new Employee("2", "bar", null, "MALE", 1.68)); 103 | employees.add(new Employee("3", "foo bar", null, null, null)); 104 | 105 | // Campanies 106 | List companies = new ArrayList(); 107 | companies.add(new Company("Google", 12000, "Palo Alto, CA")); 108 | companies.add(new Company("Facebook", null, "Mountain View, CA")); 109 | companies.add(new Company("SpaceX", null, null)); 110 | 111 | // Add Sheets 112 | gew.addSheet(Employee.class, employees); 113 | gew.addSheet(Company.class, companies); 114 | 115 | // Write 116 | gew.write(); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/reader/AbstractSpreadsheetReader.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.poi.SpreadsheetReadException; 4 | import io.github.millij.poi.ss.handler.RowListener; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | 16 | /** 17 | * A abstract implementation of {@link SpreadsheetReader}. 18 | * 19 | * @author milli, Fang Gang 20 | */ 21 | abstract class AbstractSpreadsheetReader implements SpreadsheetReader { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSpreadsheetReader.class); 24 | 25 | 26 | // Abstract Methods 27 | // ------------------------------------------------------------------------ 28 | 29 | 30 | // Methods 31 | // ------------------------------------------------------------------------ 32 | 33 | @Override 34 | public void read(Class beanClz, File file, RowListener callback) throws SpreadsheetReadException { 35 | try (InputStream fis = new FileInputStream(file)) { 36 | 37 | // chain 38 | this.read(beanClz, fis, callback); 39 | } catch (IOException ex) { 40 | String errMsg = String.format("ERR - %s", ex.getMessage()); 41 | throw new SpreadsheetReadException(errMsg, ex); 42 | } 43 | } 44 | 45 | 46 | @Override 47 | public void read(Class beanClz, File file, int sheetNo, RowListener callback) 48 | throws SpreadsheetReadException { 49 | // Sanity checks 50 | try { 51 | InputStream fis = new FileInputStream(file); 52 | 53 | // chain 54 | this.read(beanClz, fis, sheetNo, callback); 55 | } catch (IOException ex) { 56 | String errMsg = String.format("ERR - %s", ex.getMessage()); 57 | throw new SpreadsheetReadException(errMsg, ex); 58 | } 59 | } 60 | 61 | 62 | @Override 63 | public List read(Class beanClz, File file) throws SpreadsheetReadException { 64 | try (InputStream fis = new FileInputStream(file)) { 65 | return this.read(beanClz, fis); 66 | } catch (IOException ex) { 67 | String errMsg = String.format("ERR - %s", ex.getMessage()); 68 | throw new SpreadsheetReadException(errMsg, ex); 69 | } 70 | } 71 | 72 | @Override 73 | public List read(Class beanClz, InputStream is) throws SpreadsheetReadException { 74 | // Result 75 | final List sheetBeans = new ArrayList(); 76 | 77 | // Read with callback to fill list 78 | this.read(beanClz, is, new RowListener() { 79 | 80 | @Override 81 | public void row(int rowNum, T rowObj) { 82 | if (rowObj == null) { 83 | LOGGER.error("Null object returned for row : {}", rowNum); 84 | return; 85 | } 86 | 87 | sheetBeans.add(rowObj); 88 | } 89 | 90 | }); 91 | 92 | return sheetBeans; 93 | } 94 | 95 | 96 | @Override 97 | public List read(Class beanClz, File file, int sheetNo) throws SpreadsheetReadException { 98 | try (InputStream fis = new FileInputStream(file)) { 99 | return this.read(beanClz, fis, sheetNo); 100 | } catch (IOException ex) { 101 | String errMsg = String.format("ERR - %s", ex.getMessage()); 102 | throw new SpreadsheetReadException(errMsg, ex); 103 | } 104 | } 105 | 106 | @Override 107 | public List read(Class beanClz, InputStream is, int sheetNo) throws SpreadsheetReadException { 108 | // Result 109 | final List sheetBeans = new ArrayList(); 110 | 111 | // Read with callback to fill list 112 | this.read(beanClz, is, sheetNo, new RowListener() { 113 | 114 | @Override 115 | public void row(int rowNum, T rowObj) { 116 | if (rowObj == null) { 117 | LOGGER.error("Null object returned for row : {}", rowNum); 118 | return; 119 | } 120 | 121 | sheetBeans.add(rowObj); 122 | } 123 | 124 | }); 125 | 126 | return sheetBeans; 127 | } 128 | 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/reader/XlsReader.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.poi.SpreadsheetReadException; 4 | import io.github.millij.poi.ss.handler.RowListener; 5 | import io.github.millij.poi.ss.model.ColumnMapping; 6 | import io.github.millij.poi.ss.model.SheetRow; 7 | import io.github.millij.poi.util.Spreadsheet; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.poi.hssf.usermodel.HSSFRow; 10 | import org.apache.poi.hssf.usermodel.HSSFSheet; 11 | import org.apache.poi.hssf.usermodel.HSSFWorkbook; 12 | import org.apache.poi.ss.usermodel.Row; 13 | import org.apache.poi.ss.usermodel.Workbook; 14 | 15 | import java.io.InputStream; 16 | 17 | import static io.github.millij.poi.util.Beans.isInstantiableType; 18 | 19 | /** 20 | * Reader impletementation of {@link Workbook} for an POIFS file (.xls). 21 | * 22 | * @author milli, Fang Gang 23 | * @see XlsxReader 24 | */ 25 | @Slf4j 26 | public class XlsReader extends AbstractSpreadsheetReader { 27 | 28 | // Constructor 29 | 30 | public XlsReader() { 31 | super(); 32 | } 33 | 34 | 35 | // WorkbookReader Impl 36 | // ------------------------------------------------------------------------ 37 | 38 | @Override 39 | public void read(Class beanClz, InputStream is, RowListener listener) throws SpreadsheetReadException { 40 | // Sanity checks 41 | if (!isInstantiableType(beanClz)) { 42 | throw new IllegalArgumentException("XlsReader :: Invalid bean type passed!"); 43 | } 44 | 45 | String sheetName = ""; 46 | 47 | try { 48 | final HSSFWorkbook wb = new HSSFWorkbook(is); 49 | final int sheetCount = wb.getNumberOfSheets(); 50 | log.debug("Total no. of sheets found in HSSFWorkbook : #{}", sheetCount); 51 | 52 | // Iterate over sheets 53 | for (int i = 0; i < sheetCount; i++) { 54 | final HSSFSheet sheet = wb.getSheetAt(i); 55 | sheetName = sheet.getSheetName(); 56 | log.debug("Processing HSSFSheet at No. : {}", i); 57 | 58 | // Process Sheet 59 | this.processSheet(beanClz, sheet, 0, listener); 60 | } 61 | 62 | // Close workbook 63 | wb.close(); 64 | } catch (Exception ex) { 65 | log.error("HSSFSheet to Bean({}) Error - Sheet[{}] {}", beanClz.getSimpleName(), sheetName, ex.getMessage()); 66 | throw new SpreadsheetReadException(sheetName, ex); 67 | } 68 | 69 | } 70 | 71 | @Override 72 | public void read(Class beanClz, InputStream is, int sheetNo, RowListener listener) 73 | throws SpreadsheetReadException { 74 | // Sanity checks 75 | if (!isInstantiableType(beanClz)) { 76 | throw new IllegalArgumentException("XlsReader :: Invalid bean type passed!"); 77 | } 78 | 79 | String sheetName = ""; 80 | 81 | try { 82 | HSSFWorkbook wb = new HSSFWorkbook(is); 83 | final HSSFSheet sheet = wb.getSheetAt(sheetNo); 84 | sheetName = sheet.getSheetName(); 85 | 86 | // Process Sheet 87 | this.processSheet(beanClz, sheet, 0, listener); 88 | 89 | // Close workbook 90 | wb.close(); 91 | } catch (Exception ex) { 92 | log.error("HSSFSheet to Bean({}) Error - Sheet[{}] {}", beanClz.getSimpleName(), sheetName, ex.getMessage()); 93 | throw new SpreadsheetReadException(sheetName, ex); 94 | } 95 | } 96 | 97 | 98 | // Sheet Process 99 | 100 | protected void processSheet(Class beanClz, HSSFSheet sheet, int headerRowNo, RowListener rowListener) { 101 | // Get header row data 102 | final SheetRow headerRow = SheetRow.buildFromHSSFRow(sheet.getRow(headerRowNo)); 103 | final ColumnMapping columnMapping = new ColumnMapping(beanClz, headerRow); 104 | 105 | for (Row row : sheet) { 106 | // Process Row Data 107 | int rowNum = row.getRowNum(); 108 | // Skip Header row 109 | if (rowNum <= 0) { 110 | continue; 111 | } 112 | 113 | SheetRow sheetRow = SheetRow.buildFromHSSFRow((HSSFRow) row); 114 | if (sheetRow.isEmpty()) { 115 | log.warn("Row(idx{}) data is empty.", row.getRowNum()); 116 | continue; 117 | } 118 | 119 | // Row data as Bean 120 | T rowBean = Spreadsheet.rowAsBean(sheetRow, columnMapping); 121 | // Row Callback 122 | rowListener.row(rowNum, rowBean); 123 | } 124 | } 125 | 126 | 127 | // Private Methods 128 | // ------------------------------------------------------------------------ 129 | 130 | } 131 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/writer/SpreadsheetWriter.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.writer; 2 | 3 | import io.github.millij.poi.ss.model.annotations.Sheet; 4 | import io.github.millij.poi.util.Spreadsheet; 5 | 6 | import java.io.File; 7 | import java.io.FileNotFoundException; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | import java.util.ArrayList; 12 | import java.util.LinkedHashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import org.apache.commons.collections.CollectionUtils; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.apache.poi.ss.usermodel.Cell; 19 | import org.apache.poi.xssf.usermodel.XSSFCell; 20 | import org.apache.poi.xssf.usermodel.XSSFRow; 21 | import org.apache.poi.xssf.usermodel.XSSFSheet; 22 | import org.apache.poi.xssf.usermodel.XSSFWorkbook; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | 27 | @Deprecated 28 | public class SpreadsheetWriter { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(SpreadsheetWriter.class); 31 | 32 | private final XSSFWorkbook workbook; 33 | private final OutputStream outputStrem; 34 | 35 | 36 | // Constructors 37 | // ------------------------------------------------------------------------ 38 | 39 | public SpreadsheetWriter(String filepath) throws FileNotFoundException { 40 | this(new File(filepath)); 41 | } 42 | 43 | public SpreadsheetWriter(File file) throws FileNotFoundException { 44 | this(new FileOutputStream(file)); 45 | } 46 | 47 | public SpreadsheetWriter(OutputStream outputStream) { 48 | super(); 49 | 50 | this.workbook = new XSSFWorkbook(); 51 | this.outputStrem = outputStream; 52 | } 53 | 54 | 55 | // Methods 56 | // ------------------------------------------------------------------------ 57 | 58 | 59 | // Sheet :: Add 60 | 61 | public void addSheet(Class beanType, List rowObjects) { 62 | // Sheet Headers 63 | List headers = Spreadsheet.getColumnNames(beanType); 64 | 65 | this.addSheet(beanType, rowObjects, headers); 66 | } 67 | 68 | public void addSheet(Class beanType, List rowObjects, List headers) { 69 | // SheetName 70 | Sheet sheet = beanType.getAnnotation(Sheet.class); 71 | String sheetName = sheet != null ? sheet.value() : null; 72 | 73 | this.addSheet(beanType, rowObjects, headers, sheetName); 74 | } 75 | 76 | public void addSheet(Class beanType, List rowObjects, String sheetName) { 77 | // Sheet Headers 78 | List headers = Spreadsheet.getColumnNames(beanType); 79 | 80 | this.addSheet(beanType, rowObjects, headers, sheetName); 81 | } 82 | 83 | public void addSheet(Class beanType, List rowObjects, List headers, 84 | String sheetName) { 85 | // Sanity checks 86 | if (beanType == null) { 87 | throw new IllegalArgumentException("GenericExcelWriter :: ExcelBean type should not be null"); 88 | } 89 | 90 | if (CollectionUtils.isEmpty(rowObjects)) { 91 | LOGGER.error("Skipping excel sheet writing as the ExcelBean collection is empty"); 92 | return; 93 | } 94 | 95 | if (CollectionUtils.isEmpty(headers)) { 96 | LOGGER.error("Skipping excel sheet writing as the headers collection is empty"); 97 | return; 98 | } 99 | 100 | try { 101 | XSSFSheet exSheet = workbook.getSheet(sheetName); 102 | if (exSheet != null) { 103 | String errMsg = String.format("A Sheet with the passed name already exists : %s", sheetName); 104 | throw new IllegalArgumentException(errMsg); 105 | } 106 | 107 | XSSFSheet sheet = StringUtils.isEmpty(sheetName) ? workbook.createSheet() : workbook.createSheet(sheetName); 108 | LOGGER.debug("Added new Sheet[name] to the workbook : {}", sheet.getSheetName()); 109 | 110 | // Header 111 | XSSFRow headerRow = sheet.createRow(0); 112 | for (int i = 0; i < headers.size(); i++) { 113 | XSSFCell cell = headerRow.createCell(i); 114 | cell.setCellValue(headers.get(i)); 115 | } 116 | 117 | // Data Rows 118 | Map> rowsData = this.prepareSheetRowsData(headers, rowObjects); 119 | for (int i = 0, rowNum = 1; i < rowObjects.size(); i++, rowNum++) { 120 | final XSSFRow row = sheet.createRow(rowNum); 121 | 122 | int cellNo = 0; 123 | for (String key : rowsData.keySet()) { 124 | Cell cell = row.createCell(cellNo); 125 | String value = rowsData.get(key).get(i); 126 | cell.setCellValue(value); 127 | cellNo++; 128 | } 129 | } 130 | 131 | } catch (Exception ex) { 132 | String errMsg = String.format("Error while preparing sheet with passed row objects : %s", ex.getMessage()); 133 | LOGGER.error(errMsg, ex); 134 | } 135 | } 136 | 137 | 138 | // Sheet :: Append to existing 139 | 140 | 141 | 142 | // Write 143 | 144 | public void write() throws IOException { 145 | workbook.write(outputStrem); 146 | workbook.close(); 147 | } 148 | 149 | 150 | // Private Methods 151 | // ------------------------------------------------------------------------ 152 | 153 | private Map> prepareSheetRowsData(List headers, 154 | List rowObjects) throws Exception { 155 | 156 | final Map> sheetData = new LinkedHashMap>(); 157 | 158 | // Iterate over Objects 159 | for (EB excelBean : rowObjects) { 160 | Map row = Spreadsheet.asRowDataMap(excelBean, headers); 161 | 162 | for (String header : headers) { 163 | List data = sheetData.containsKey(header) ? sheetData.get(header) : new ArrayList(); 164 | String value = row.get(header) != null ? row.get(header) : ""; 165 | data.add(value); 166 | 167 | sheetData.put(header, data); 168 | } 169 | } 170 | 171 | return sheetData; 172 | } 173 | 174 | 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/poi/ss/reader/XlsReaderTest.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.bean.Company; 4 | import io.github.millij.bean.Employee; 5 | import io.github.millij.poi.SpreadsheetReadException; 6 | import io.github.millij.poi.ss.handler.RowListener; 7 | import io.github.millij.poi.ss.reader.XlsReader; 8 | 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.InputStream; 13 | import java.text.ParseException; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import org.junit.After; 18 | import org.junit.Assert; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | 25 | public class XlsReaderTest { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(XlsReaderTest.class); 28 | 29 | // XLS 30 | private String _filepath_xls_single_sheet; 31 | private String _filepath_xls_multiple_sheets; 32 | 33 | // Setup 34 | // ------------------------------------------------------------------------ 35 | 36 | @Before 37 | public void setup() throws ParseException { 38 | // filepaths 39 | 40 | // xls 41 | _filepath_xls_single_sheet = "src/test/resources/sample-files/xls_sample_single_sheet.xls"; 42 | _filepath_xls_multiple_sheets = "src/test/resources/sample-files/xls_sample_multiple_sheets.xls"; 43 | } 44 | 45 | @After 46 | public void teardown() { 47 | // nothing to do 48 | } 49 | 50 | 51 | // Tests 52 | // ------------------------------------------------------------------------ 53 | 54 | 55 | // Read from file 56 | 57 | @Test 58 | public void test_read_xls_single_sheet() throws SpreadsheetReadException { 59 | // Excel Reader 60 | LOGGER.info("test_read_xls_single_sheet :: Reading file - {}", _filepath_xls_single_sheet); 61 | XlsReader reader = new XlsReader(); 62 | 63 | // Read 64 | List employees = reader.read(Employee.class, new File(_filepath_xls_single_sheet)); 65 | Assert.assertNotNull(employees); 66 | Assert.assertTrue(employees.size() > 0); 67 | 68 | for (Employee emp : employees) { 69 | LOGGER.info("test_read_xls_single_sheet :: Output - {}", emp); 70 | } 71 | } 72 | 73 | @Test 74 | public void test_read_xls_multiple_sheets() throws SpreadsheetReadException { 75 | // Excel Reader 76 | LOGGER.info("test_read_xlsx_multiple_sheets :: Reading file - {}", _filepath_xls_multiple_sheets); 77 | XlsReader reader = new XlsReader(); 78 | 79 | // Read Sheet 1 80 | List employees = reader.read(Employee.class, new File(_filepath_xls_multiple_sheets), 0); 81 | Assert.assertNotNull(employees); 82 | Assert.assertTrue(employees.size() > 0); 83 | 84 | for (Employee emp : employees) { 85 | LOGGER.info("test_read_xls_multiple_sheets :: Output - {}", emp); 86 | } 87 | 88 | // Read Sheet 2 89 | List companies = reader.read(Company.class, new File(_filepath_xls_multiple_sheets), 1); 90 | Assert.assertNotNull(companies); 91 | Assert.assertTrue(companies.size() > 0); 92 | 93 | for (Company company : companies) { 94 | LOGGER.info("test_read_xls_multiple_sheets :: Output - {}", company); 95 | } 96 | } 97 | 98 | 99 | 100 | // Read from Stream 101 | 102 | @Test 103 | public void test_read_xls_single_sheet_from_stream() throws SpreadsheetReadException, FileNotFoundException { 104 | // Excel Reader 105 | LOGGER.info("test_read_xls_single_sheet_from_stream :: Reading file - {}", _filepath_xls_single_sheet); 106 | XlsReader reader = new XlsReader(); 107 | 108 | // InputStream 109 | final InputStream fis = new FileInputStream(new File(_filepath_xls_single_sheet)); 110 | 111 | // Read 112 | List employees = reader.read(Employee.class, fis); 113 | Assert.assertNotNull(employees); 114 | Assert.assertTrue(employees.size() > 0); 115 | 116 | for (Employee emp : employees) { 117 | LOGGER.info("test_read_xls_single_sheet :: Output - {}", emp); 118 | } 119 | } 120 | 121 | @Test 122 | public void test_read_xls_multiple_sheets_from_stream() throws SpreadsheetReadException, FileNotFoundException { 123 | // Excel Reader 124 | LOGGER.info("test_read_xls_multiple_sheets_from_stream :: Reading file - {}", _filepath_xls_multiple_sheets); 125 | XlsReader reader = new XlsReader(); 126 | 127 | // InputStream 128 | final InputStream fisSheet1 = new FileInputStream(new File(_filepath_xls_multiple_sheets)); 129 | 130 | // Read Sheet 1 131 | List employees = reader.read(Employee.class, fisSheet1, 0); 132 | Assert.assertNotNull(employees); 133 | Assert.assertTrue(employees.size() > 0); 134 | 135 | for (Employee emp : employees) { 136 | LOGGER.info("test_read_xls_multiple_sheets :: Output - {}", emp); 137 | } 138 | 139 | // InputStream 140 | final InputStream fisSheet2 = new FileInputStream(new File(_filepath_xls_multiple_sheets)); 141 | 142 | // Read Sheet 2 143 | List companies = reader.read(Company.class, fisSheet2, 1); 144 | Assert.assertNotNull(companies); 145 | Assert.assertTrue(companies.size() > 0); 146 | 147 | for (Company company : companies) { 148 | LOGGER.info("test_read_xls_multiple_sheets :: Output - {}", company); 149 | } 150 | } 151 | 152 | 153 | // Read with Callback 154 | 155 | @Test 156 | public void test_read_xls_single_sheet_with_callback() throws SpreadsheetReadException { 157 | // Excel Reader 158 | LOGGER.info("test_read_xls_single_sheet_with_callback :: Reading file - {}", _filepath_xls_single_sheet); 159 | 160 | // file 161 | final File xlsxFile = new File(_filepath_xls_single_sheet); 162 | 163 | final List employees = new ArrayList(); 164 | 165 | // Read 166 | XlsReader reader = new XlsReader(); 167 | reader.read(Employee.class, xlsxFile, new RowListener() { 168 | 169 | @Override 170 | public void row(int rowNum, Employee employee) { 171 | employees.add(employee); 172 | LOGGER.info("test_read_xls_single_sheet_with_callback :: Output - {}", employee); 173 | 174 | } 175 | }); 176 | 177 | Assert.assertNotNull(employees); 178 | Assert.assertTrue(employees.size() > 0); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/model/SheetRow.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.model; 2 | 3 | import io.github.millij.poi.ColumnDuplicateException; 4 | import io.github.millij.poi.ColumnNotFoundException; 5 | import io.github.millij.poi.UnsupportedException; 6 | import io.github.millij.poi.util.Spreadsheet; 7 | import lombok.*; 8 | import lombok.experimental.FieldDefaults; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.apache.poi.hssf.usermodel.HSSFRow; 12 | import org.apache.poi.ss.util.CellAddress; 13 | 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.Set; 18 | 19 | /** 20 | * Sheet row wrapper. 21 | * 22 | * @author Fang Gang 23 | */ 24 | @Slf4j 25 | @ToString 26 | @FieldDefaults(level = AccessLevel.PRIVATE) 27 | public class SheetRow { 28 | 29 | /** 30 | * Row number. 31 | */ 32 | @Getter 33 | int rowNum; 34 | 35 | /** 36 | * Cell column reference to cell object. 37 | *

38 | * Example: 39 | *

 40 |      * "A" -> Cell("1000"), "B" -> Cell("James")
 41 |      * 
42 | */ 43 | Map cellColumnRefToCell; 44 | 45 | public SheetRow(int rowNum) { 46 | this.rowNum = rowNum; 47 | this.cellColumnRefToCell = new HashMap<>(); 48 | } 49 | 50 | /** 51 | * Put a cell object to sheet row object. 52 | * 53 | * @param cellReference cell reference, ex. A1, B3. 54 | * @param cellValue cell value 55 | */ 56 | public void addCell(@NonNull String cellReference, Object cellValue) { 57 | log.debug("Put cell: {}, {}", cellReference, cellValue); 58 | Cell cell = new Cell(cellReference, cellValue); 59 | String colRef = Spreadsheet.getCellColumnReference(cellReference); 60 | 61 | cellColumnRefToCell.put(colRef, cell); 62 | } 63 | 64 | /** 65 | * Put a cell object to sheet row object. 66 | * 67 | * @param cellAddress CellAddress object 68 | * @param cellValue cell value 69 | */ 70 | public void addCell(@NonNull CellAddress cellAddress, Object cellValue) { 71 | log.debug("Put cell: {}, {}", cellAddress, cellValue); 72 | Cell cell = new Cell(cellAddress, cellValue); 73 | String colRef = Spreadsheet.getCellColumnReference(cellAddress.toString()); 74 | 75 | cellColumnRefToCell.put(colRef, cell); 76 | } 77 | 78 | /** 79 | * Get a cell object by cell column reference. 80 | * 81 | * @param cellColRef cell column reference. 82 | * @return return a sheet cell model. 83 | */ 84 | public Cell getCell(@NonNull String cellColRef) { 85 | Cell cell = cellColumnRefToCell.get(cellColRef); 86 | return cell == null ? new Cell() : cell; 87 | } 88 | 89 | public Set getCellColRefs() { 90 | return cellColumnRefToCell.keySet(); 91 | } 92 | 93 | public int getPhysicalRowNum() { 94 | return rowNum + 1; 95 | } 96 | 97 | /** 98 | * Get column name to reference mapping, just for the header row. 99 | * 100 | * @return return column name to reference mapping 101 | */ 102 | public Map getColumnNameToReferenceMap() { 103 | final SheetRow headerRow = this; 104 | if (headerRow.isEmpty()) { 105 | throw new ColumnNotFoundException(headerRow.getRowNum()); 106 | } 107 | Map mapping = new HashMap<>(cellColumnRefToCell.size()); 108 | for (String ref : headerRow.getCellColRefs()) { 109 | final Cell cell = headerRow.getCell(ref); 110 | // Cell value in header row is the column/header name 111 | final String columnName = String.valueOf(cell.getValue()); 112 | // Column refuse duplication 113 | if (mapping.get(columnName) != null) { 114 | throw new ColumnDuplicateException(cell.getAddress().toString(), columnName); 115 | } 116 | mapping.put(columnName, ref); 117 | } 118 | return Collections.unmodifiableMap(mapping); 119 | } 120 | 121 | /** 122 | * Determine if all cells in a row are valid. 123 | * 124 | * @return Return true if there are valid cells in the row. 125 | */ 126 | public boolean isEmpty() { 127 | return cellColumnRefToCell == null || cellColumnRefToCell.isEmpty(); 128 | } 129 | 130 | public static SheetRow buildFromHSSFRow(HSSFRow hssfRow) { 131 | // Sanity checks 132 | if (hssfRow == null) { 133 | return null; 134 | } 135 | 136 | SheetRow sheetRow = new SheetRow(hssfRow.getRowNum()); 137 | 138 | for (org.apache.poi.ss.usermodel.Cell cell : hssfRow) { 139 | // Process cell value 140 | switch (cell.getCellType()) { 141 | case STRING: 142 | sheetRow.addCell(cell.getAddress(), cell.getStringCellValue()); 143 | break; 144 | case NUMERIC: 145 | sheetRow.addCell(cell.getAddress(), cell.getNumericCellValue()); 146 | break; 147 | case BOOLEAN: 148 | sheetRow.addCell(cell.getAddress(), cell.getBooleanCellValue()); 149 | break; 150 | case FORMULA: 151 | var err1 = String.format("Formula cell(%s) type not support.", cell.getAddress()); 152 | throw new UnsupportedException(err1); 153 | case BLANK: 154 | log.warn("Cell(%s) data is BLANK.", cell.getAddress()); 155 | break; 156 | case ERROR: 157 | var err2 = String.format("Cell(%s) data not support.", cell.getAddress()); 158 | throw new UnsupportedException(err2); 159 | default: 160 | break; 161 | } 162 | } 163 | return sheetRow; 164 | } 165 | 166 | /** 167 | * Sheet cell model. 168 | */ 169 | @Getter 170 | @Setter 171 | @ToString 172 | @AllArgsConstructor 173 | @NoArgsConstructor 174 | @FieldDefaults(level = AccessLevel.PRIVATE) 175 | public static class Cell { 176 | 177 | /** 178 | * Cell Address 179 | */ 180 | CellAddress address; 181 | 182 | /** 183 | * Cell formatted value object 184 | */ 185 | Object value; 186 | 187 | public Cell(String cellReference, Object value) { 188 | if (StringUtils.isBlank(cellReference)) { 189 | throw new IllegalArgumentException("For input string: " + cellReference); 190 | } 191 | try { 192 | this.address = new CellAddress(cellReference); 193 | } catch (NumberFormatException e) { 194 | String errMag = String.format("Cell address format failed. cell reference: %s", cellReference); 195 | throw new UnsupportedException(errMag); 196 | } 197 | this.value = value; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/util/Spreadsheet.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.util; 2 | 3 | import io.github.millij.poi.CellEmptyException; 4 | import io.github.millij.poi.UnsupportedException; 5 | import io.github.millij.poi.ss.model.ColumnMapping; 6 | import io.github.millij.poi.ss.model.SheetRow; 7 | import io.github.millij.poi.ss.model.annotations.SheetColumn; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.beanutils.BeanUtils; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | import java.lang.reflect.Field; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.lang.reflect.Method; 15 | import java.util.*; 16 | 17 | /** 18 | * Spreadsheet related utilites. 19 | */ 20 | @Slf4j 21 | public final class Spreadsheet { 22 | 23 | private Spreadsheet() { 24 | // Utility Class 25 | } 26 | 27 | 28 | // Utilities 29 | // ------------------------------------------------------------------------ 30 | 31 | /** 32 | * Splits the CellReference and returns only the column reference. 33 | * 34 | * @param cellRef the cell reference value (ex. D3) 35 | * @return returns the column index "D" from the cell reference "D3" 36 | */ 37 | public static String getCellColumnReference(String cellRef) { 38 | String cellColRef = cellRef.split("[0-9]*$")[0]; 39 | return cellColRef; 40 | } 41 | 42 | 43 | // Bean :: Property Utils 44 | 45 | public static Map getPropertyToColumnNameMap(Class beanType) { 46 | // Sanity checks 47 | if (beanType == null) { 48 | throw new IllegalArgumentException("getPropertyToColumnNameMap :: Invalid ExcelBean type - " + beanType); 49 | } 50 | 51 | // Property to Column name Mapping 52 | final Map mapping = new HashMap(); 53 | 54 | // Fields 55 | Field[] fields = beanType.getDeclaredFields(); 56 | for (Field f : fields) { 57 | String fieldName = f.getName(); 58 | mapping.put(fieldName, fieldName); 59 | 60 | SheetColumn ec = f.getAnnotation(SheetColumn.class); 61 | if (ec != null && StringUtils.isNotEmpty(ec.value())) { 62 | mapping.put(fieldName, ec.value()); 63 | } 64 | } 65 | 66 | // Methods 67 | Method[] methods = beanType.getDeclaredMethods(); 68 | for (Method m : methods) { 69 | String fieldName = Beans.getFieldName(m); 70 | if (!mapping.containsKey(fieldName)) { 71 | mapping.put(fieldName, fieldName); 72 | } 73 | 74 | SheetColumn ec = m.getAnnotation(SheetColumn.class); 75 | if (ec != null && StringUtils.isNotEmpty(ec.value())) { 76 | mapping.put(fieldName, ec.value()); 77 | } 78 | } 79 | 80 | log.info("Bean property to Excel Column of - {} : {}", beanType, mapping); 81 | return Collections.unmodifiableMap(mapping); 82 | } 83 | 84 | public static List getColumnNames(Class beanType) { 85 | // Bean Property to Column Mapping 86 | final Map propToColumnMap = getPropertyToColumnNameMap(beanType); 87 | 88 | final ArrayList columnNames = new ArrayList<>(propToColumnMap.values()); 89 | return columnNames; 90 | } 91 | 92 | // Read from Bean : as Row Data 93 | // ------------------------------------------------------------------------ 94 | 95 | public static Map asRowDataMap(Object beanObj, List colHeaders) throws Exception { 96 | // Excel Bean Type 97 | final Class beanType = beanObj.getClass(); 98 | 99 | // RowData map 100 | final Map rowDataMap = new HashMap(); 101 | 102 | // Fields 103 | for (Field f : beanType.getDeclaredFields()) { 104 | if (!f.isAnnotationPresent(SheetColumn.class)) { 105 | continue; 106 | } 107 | 108 | String fieldName = f.getName(); 109 | 110 | SheetColumn ec = f.getAnnotation(SheetColumn.class); 111 | String header = StringUtils.isEmpty(ec.value()) ? fieldName : ec.value(); 112 | if (!colHeaders.contains(header)) { 113 | continue; 114 | } 115 | 116 | rowDataMap.put(header, Beans.getFieldValueAsString(beanObj, fieldName)); 117 | } 118 | 119 | // Methods 120 | for (Method m : beanType.getDeclaredMethods()) { 121 | if (!m.isAnnotationPresent(SheetColumn.class)) { 122 | continue; 123 | } 124 | 125 | String fieldName = Beans.getFieldName(m); 126 | 127 | SheetColumn ec = m.getAnnotation(SheetColumn.class); 128 | String header = StringUtils.isEmpty(ec.value()) ? fieldName : ec.value(); 129 | if (!colHeaders.contains(header)) { 130 | continue; 131 | } 132 | 133 | rowDataMap.put(header, Beans.getFieldValueAsString(beanObj, fieldName)); 134 | } 135 | 136 | return rowDataMap; 137 | } 138 | 139 | 140 | // Write to Bean :: from Row data 141 | // ------------------------------------------------------------------------ 142 | 143 | public static T rowAsBean(SheetRow sheetRow, ColumnMapping columnMapping) { 144 | 145 | final Class beanClz = columnMapping.getBeanClz(); 146 | // Sanity checks 147 | if (beanClz == null || columnMapping.isEmpty() || sheetRow.isEmpty()) { 148 | log.warn("Mapping row as bean failed, beanClz - {}, colRefToProperty - {}, dataRow - {}.", 149 | beanClz.getSimpleName(), columnMapping, sheetRow); 150 | return null; 151 | } 152 | 153 | T rowBean = newBeanInstance(beanClz); 154 | 155 | // Fill in the data 156 | for (String colName : columnMapping.getCellColNames()) { 157 | 158 | ColumnMapping.Property property = columnMapping.get(colName); 159 | final String colRef = property.getColumnReference(); 160 | Object cellValue = sheetRow.getCell(colRef).getValue(); 161 | 162 | if (!property.isNullable()) { 163 | if (cellValue == null || StringUtils.isEmpty(cellValue.toString())) { 164 | final String cellRef = colRef + sheetRow.getPhysicalRowNum(); 165 | throw new CellEmptyException(cellRef, property.getColumnName()); 166 | } 167 | } 168 | 169 | try { 170 | // Set the property value in the current row object bean 171 | BeanUtils.setProperty(rowBean, property.getFieldName(), cellValue); 172 | } catch (IllegalAccessException | InvocationTargetException ex) { 173 | String errMsg = String.format("Failed to set bean property - %s, value - %s, sheetRow - %s.", 174 | property.getFieldName(), cellValue, sheetRow); 175 | log.error(errMsg, ex); 176 | throw new UnsupportedException(errMsg); 177 | } 178 | 179 | } 180 | return rowBean; 181 | } 182 | 183 | private static T newBeanInstance(Class beanClz) { 184 | T rowBean; 185 | try { 186 | // Create new Instance 187 | rowBean = beanClz.newInstance(); 188 | } catch (InstantiationException | IllegalAccessException ex) { 189 | String errMsg = String.format("Error while creating bean - %s.", beanClz); 190 | log.error(errMsg, ex); 191 | throw new UnsupportedException(errMsg); 192 | } 193 | return rowBean; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/model/ColumnMapping.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.model; 2 | 3 | import io.github.millij.poi.ColumnNotFoundException; 4 | import io.github.millij.poi.ss.model.annotations.SheetColumn; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NonNull; 8 | import lombok.ToString; 9 | import lombok.experimental.FieldDefaults; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | import java.lang.reflect.Field; 13 | import java.util.*; 14 | 15 | /** 16 | * 17 | * @param Class Type 18 | * @author Fang Gang 19 | */ 20 | @ToString 21 | @FieldDefaults(level = AccessLevel.PRIVATE) 22 | public class ColumnMapping { 23 | 24 | @Getter 25 | final Class beanClz; 26 | final Map columnNameToProperty; 27 | 28 | public ColumnMapping(Class beanClz, SheetRow headerRow) { 29 | this.beanClz = beanClz; 30 | this.columnNameToProperty = new HashMap<>(); 31 | 32 | init(beanClz); 33 | checkAndMapping(headerRow); 34 | } 35 | 36 | /** 37 | * Get Bean Property by sheet column name. 38 | * 39 | * @param columnName sheet column name 40 | * @return return Property object. 41 | */ 42 | public Property get(@NonNull String columnName) { 43 | return columnNameToProperty.get(columnName); 44 | } 45 | 46 | public Set getCellColNames() { 47 | if (columnNameToProperty == null) { 48 | return new HashSet<>(); 49 | } 50 | return columnNameToProperty.keySet(); 51 | } 52 | 53 | public boolean isEmpty() { 54 | return columnNameToProperty == null || columnNameToProperty.isEmpty(); 55 | } 56 | 57 | // Private Methods 58 | // ------------------------------------------------------------------------ 59 | 60 | private void init(Class beanClz) { 61 | // Sanity checks 62 | if (beanClz == null) { 63 | throw new IllegalArgumentException("Error :: Invalid Excel Bean Type - null"); 64 | } 65 | 66 | // Fields 67 | Field[] fields = beanClz.getDeclaredFields(); 68 | for (Field f : fields) { 69 | final SheetColumn fa = f.getAnnotation(SheetColumn.class); 70 | this.set(new Property(f.getName(), fa)); 71 | } 72 | 73 | // Methods 74 | /* 75 | Method[] methods = beanClz.getDeclaredMethods(); 76 | for (Method m : methods) { 77 | final String fieldName = Beans.getFieldName(m); 78 | final Boolean canCover = fieldAnnotationMap.get(fieldName); 79 | if (canCover == null || canCover) { 80 | columnNameToProperty.remove(fieldName); 81 | this.set(new Property(fieldName, m.getAnnotation(SheetColumn.class))); 82 | } 83 | } 84 | */ 85 | } 86 | 87 | private void set(final Property property) { 88 | columnNameToProperty.put(property.getColumnName(), property); 89 | } 90 | 91 | /** 92 | * Check the validation of column in the header row, 93 | * then setup the property - columnReference. 94 | * 95 | * @param headerRow sheet header row data 96 | */ 97 | private void checkAndMapping(@NonNull SheetRow headerRow) { 98 | // Get sheet header name to column reference mapping 99 | Map nameToReferenceMap = headerRow.getColumnNameToReferenceMap(); 100 | 101 | removeNotNeedKV(nameToReferenceMap); 102 | 103 | Iterator> it = columnNameToProperty.entrySet().iterator(); 104 | while (it.hasNext()) { 105 | Map.Entry entry = it.next(); 106 | final String colName = entry.getKey(); 107 | final Property property = entry.getValue(); 108 | final String colRef = nameToReferenceMap.get(colName); 109 | 110 | if (StringUtils.isNotEmpty(colRef)) { 111 | 112 | property.columnReference = colRef; 113 | } else { 114 | 115 | if (property.isNullable()) { 116 | // If not found the reference and property.nullable=true, remove it 117 | it.remove(); 118 | continue; 119 | } else { 120 | throw new ColumnNotFoundException(headerRow.getRowNum(), colName); 121 | } 122 | } 123 | } 124 | } 125 | 126 | private void removeNotNeedKV(Map nameToReferenceMap) { 127 | Map notFound = new HashMap<>(); 128 | Map found = new HashMap<>(); 129 | boolean flag = false; 130 | 131 | for (String colName : columnNameToProperty.keySet()) { 132 | final Property property = columnNameToProperty.get(colName); 133 | final String colRef = nameToReferenceMap.get(colName); 134 | if (property.exclusive) { 135 | flag = true; 136 | if (StringUtils.isNotEmpty(colRef)) { 137 | found.put(colName, colRef); 138 | } else { 139 | notFound.put(colName, colRef); 140 | } 141 | } 142 | } 143 | 144 | if (flag) { 145 | if (found.size() <= 0) { 146 | throwException(notFound, "One of the columns must be included: "); 147 | } else if (found.size() == 1) { 148 | removeExclusiveColumn(notFound); 149 | } else { 150 | throwException(found, "These columns cannot coexist: "); 151 | } 152 | } 153 | } 154 | 155 | private void throwException(Map found, String s) { 156 | StringBuilder msgBuilder = new StringBuilder(s); 157 | final Iterator iterator = found.keySet().iterator(); 158 | while (iterator.hasNext()) { 159 | msgBuilder.append(iterator.next()); 160 | if (iterator.hasNext()) { 161 | msgBuilder.append(", "); 162 | } else { 163 | msgBuilder.append("."); 164 | } 165 | } 166 | // TODO refactor it 167 | throw new RuntimeException(msgBuilder.toString()); 168 | } 169 | 170 | private void removeExclusiveColumn(Map notFound) { 171 | for (String colName : notFound.keySet()) { 172 | columnNameToProperty.remove(colName); 173 | } 174 | } 175 | 176 | private boolean columnIsNotExist(String fieldName) { 177 | return !columnNameToProperty.containsKey(fieldName); 178 | } 179 | 180 | 181 | @Getter 182 | @ToString 183 | @FieldDefaults(level = AccessLevel.PRIVATE) 184 | public static class Property { 185 | /** 186 | * Property name 187 | */ 188 | String fieldName; 189 | 190 | /** 191 | * Name of the column to map the annotated property with. 192 | */ 193 | String columnName; 194 | 195 | /** 196 | * The reference of the column, ex. A or F 197 | */ 198 | String columnReference; 199 | 200 | /** 201 | * Property can be null 202 | */ 203 | boolean nullable; 204 | 205 | /** 206 | * Exclusive column flag, columns that are also set to true cannot exist at the same time. 207 | */ 208 | boolean exclusive; 209 | 210 | protected Property(String fieldName, SheetColumn fa) { 211 | if (fa == null || StringUtils.isEmpty(fa.value())) { 212 | this.fieldName = fieldName; 213 | this.columnName = fieldName; 214 | this.nullable = true; 215 | this.exclusive = false; 216 | } else { 217 | this.fieldName = fieldName; 218 | this.columnName = fa.value(); 219 | this.nullable = fa.nullable(); 220 | this.exclusive = fa.exclusive(); 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/io/github/millij/poi/ss/reader/SpreadsheetReader.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.poi.SpreadsheetReadException; 4 | import io.github.millij.poi.ss.handler.RowListener; 5 | 6 | import java.io.File; 7 | import java.io.InputStream; 8 | import java.util.List; 9 | 10 | 11 | /** 12 | * An Abstract representation of a Spreadsheet Reader. Any reader implementation (HSSF or XSSF) is 13 | * expected to implement and provide the below APIs. 14 | * 15 | * @author milli 16 | */ 17 | public interface SpreadsheetReader { 18 | 19 | 20 | // Read with Custom RowListener 21 | 22 | /** 23 | * Reads the spreadsheet file to beans of the given type. This method will attempt to read all 24 | * the available sheets of the file and creates the objects of the passed type. 25 | * 26 | *

27 | * The {@link RowListener} implementation callback gets triggered after reading each Row. Best 28 | * Suited for reading Large files in restricted memory environments. 29 | *

30 | * 31 | * @param The Parameterized bean Class. 32 | * @param beanClz The Class type to deserialize the rows data 33 | * @param file {@link File} object of the spreadsheet file 34 | * @param listener Custom {@link RowListener} implementation for row data callbacks. 35 | * 36 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 37 | * readable or row data to bean mapping failed. 38 | */ 39 | void read(Class beanClz, File file, RowListener listener) throws SpreadsheetReadException; 40 | 41 | 42 | /** 43 | * Reads the spreadsheet file to beans of the given type. This method will attempt to read all 44 | * the available sheets of the file and creates the objects of the passed type. 45 | * 46 | *

47 | * The {@link RowListener} implementation callback gets triggered after reading each Row. Best 48 | * Suited for reading Large files in restricted memory environments. 49 | *

50 | * 51 | * @param The Parameterized bean Class. 52 | * @param beanClz The Class type to deserialize the rows data 53 | * @param is {@link InputStream} of the spreadsheet file 54 | * @param listener Custom {@link RowListener} implementation for row data callbacks. 55 | * 56 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 57 | * readable or row data to bean mapping failed. 58 | */ 59 | void read(Class beanClz, InputStream is, RowListener listener) throws SpreadsheetReadException; 60 | 61 | 62 | /** 63 | * Reads the spreadsheet file to beans of the given type. Note that only the requested sheet 64 | * (sheet numbers are indexed from 0) will be read. 65 | * 66 | *

67 | * The {@link RowListener} implementation callback gets triggered after reading each Row. Best 68 | * Suited for reading Large files in restricted memory environments. 69 | *

70 | * 71 | * @param The Parameterized bean Class. 72 | * @param beanClz The Class type to deserialize the rows data 73 | * @param file {@link File} object of the spreadsheet file 74 | * @param sheetNo index of the Sheet to be read (index starts from 0) 75 | * @param listener Custom {@link RowListener} implementation for row data callbacks. 76 | * 77 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 78 | * readable or row data to bean mapping failed. 79 | */ 80 | void read(Class beanClz, File file, int sheetNo, RowListener listener) throws SpreadsheetReadException; 81 | 82 | 83 | /** 84 | * Reads the spreadsheet file to beans of the given type. Note that only the requested sheet 85 | * (sheet numbers are indexed from 0) will be read. 86 | * 87 | *

88 | * The {@link RowListener} implementation callback gets triggered after reading each Row. Best 89 | * Suited for reading Large files in restricted memory environments. 90 | *

91 | * 92 | * @param The Parameterized bean Class. 93 | * @param beanClz The Class type to deserialize the rows data 94 | * @param is {@link InputStream} of the spreadsheet file 95 | * @param sheetNo index of the Sheet to be read (index starts from 0) 96 | * @param listener Custom {@link RowListener} implementation for row data callbacks. 97 | * 98 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 99 | * readable or row data to bean mapping failed. 100 | */ 101 | void read(Class beanClz, InputStream is, int sheetNo, RowListener listener) 102 | throws SpreadsheetReadException; 103 | 104 | 105 | 106 | // Read with default RowListener 107 | 108 | /** 109 | * Reads the spreadsheet file to beans of the given type. This method will attempt to read all 110 | * the available sheets of the file and creates the objects of the passed type. 111 | * 112 | * @param The Parameterized bean Class. 113 | * @param beanClz The Class type to deserialize the rows data 114 | * @param file {@link File} object of the spreadsheet file 115 | * 116 | * @return a {@link List} of objects of the parameterized type 117 | * 118 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 119 | * readable or row data to bean mapping failed. 120 | */ 121 | List read(Class beanClz, File file) throws SpreadsheetReadException; 122 | 123 | 124 | /** 125 | * Reads the spreadsheet file to beans of the given type. This method will attempt to read all 126 | * the available sheets of the file and creates the objects of the passed type. 127 | * 128 | * @param The Parameterized bean Class. 129 | * @param beanClz The Class type to deserialize the rows data 130 | * @param is {@link InputStream} of the spreadsheet file 131 | * 132 | * @return a {@link List} of objects of the parameterized type 133 | * 134 | * @throws SpreadsheetReadException an exception is thrown in cases where the file data is not 135 | * readable or row data to bean mapping failed. 136 | */ 137 | List read(Class beanClz, InputStream is) throws SpreadsheetReadException; 138 | 139 | 140 | /** 141 | * Reads the spreadsheet file to beans of the given type. Note that only the requested sheet 142 | * (sheet numbers are indexed from 0) will be read. 143 | * 144 | * @param The Parameterized bean Class. 145 | * @param beanClz beanClz The Class type to deserialize the rows data 146 | * @param file file {@link File} object of the spreadsheet file 147 | * @param sheetNo index of the Sheet to be read (index starts from 0) 148 | * 149 | * @return a {@link List} of objects of the parameterized type 150 | * 151 | * @throws SpreadsheetReadException SpreadsheetReadException an exception is thrown in cases 152 | * where the file data is not readable or row data to bean mapping failed. 153 | */ 154 | List read(Class beanClz, File file, int sheetNo) throws SpreadsheetReadException; 155 | 156 | 157 | /** 158 | * Reads the spreadsheet file to beans of the given type. Note that only the requested sheet 159 | * (sheet numbers are indexed from 0) will be read. 160 | * 161 | * @param The Parameterized bean Class. 162 | * @param beanClz beanClz The Class type to deserialize the rows data 163 | * @param is {@link InputStream} of the spreadsheet file 164 | * @param sheetNo index of the Sheet to be read (index starts from 0) 165 | * 166 | * @return a {@link List} of objects of the parameterized type 167 | * 168 | * @throws SpreadsheetReadException SpreadsheetReadException an exception is thrown in cases 169 | * where the file data is not readable or row data to bean mapping failed. 170 | */ 171 | List read(Class beanClz, InputStream is, int sheetNo) throws SpreadsheetReadException; 172 | 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/test/java/io/github/millij/poi/ss/reader/XlsxReaderTest.java: -------------------------------------------------------------------------------- 1 | package io.github.millij.poi.ss.reader; 2 | 3 | import io.github.millij.bean.Company; 4 | import io.github.millij.bean.CompetitionData; 5 | import io.github.millij.bean.Employee; 6 | import io.github.millij.poi.SpreadsheetReadException; 7 | import io.github.millij.poi.ss.handler.RowListener; 8 | import io.github.millij.poi.ss.reader.XlsxReader; 9 | 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileNotFoundException; 13 | import java.io.InputStream; 14 | import java.text.ParseException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import org.junit.After; 19 | import org.junit.Assert; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | 26 | public class XlsxReaderTest { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(XlsxReaderTest.class); 29 | 30 | // XLSX 31 | private String _filepath_xlsx_single_sheet; 32 | private String _filepath_xlsx_multiple_sheets; 33 | private String _filepath_xlsx_competition; 34 | 35 | 36 | // Setup 37 | // ------------------------------------------------------------------------ 38 | 39 | @Before 40 | public void setup() throws ParseException { 41 | // sample files 42 | _filepath_xlsx_single_sheet = "src/test/resources/sample-files/xlsx_sample_single_sheet.xlsx"; 43 | _filepath_xlsx_multiple_sheets = "src/test/resources/sample-files/xlsx_sample_multiple_sheets.xlsx"; 44 | _filepath_xlsx_competition = "src/test/resources/sample-files/competition_data.xlsx"; 45 | } 46 | 47 | @After 48 | public void teardown() { 49 | // nothing to do 50 | } 51 | 52 | 53 | // Tests 54 | // ------------------------------------------------------------------------ 55 | 56 | 57 | // Read from file 58 | 59 | @Test 60 | public void test_read_xlsx_single_sheet() throws SpreadsheetReadException, FileNotFoundException { 61 | // Excel Reader 62 | LOGGER.info("test_read_xlsx_single_sheet :: Reading file - {}", _filepath_xlsx_single_sheet); 63 | XlsxReader reader = new XlsxReader(); 64 | 65 | // Result 66 | final List employees = new ArrayList(); 67 | // Read 68 | reader.read(Employee.class, 69 | new FileInputStream(new File(_filepath_xlsx_single_sheet)), 70 | (rowNum, rowObj) -> { 71 | if (rowObj == null) { 72 | LOGGER.error("Null object returned for row : {}", rowNum); 73 | return; 74 | } 75 | if (rowObj.getAddress() == null) { 76 | LOGGER.error("Callback: Null address returned for row : {}", rowNum); 77 | } 78 | 79 | employees.add(rowObj); 80 | }); 81 | Assert.assertNotNull(employees); 82 | Assert.assertTrue(employees.size() > 0); 83 | 84 | for (Employee emp : employees) { 85 | LOGGER.info("test_read_xlsx_single_sheet :: Output - {}", emp); 86 | } 87 | } 88 | 89 | @Test 90 | public void test_read_competition_xlsx() throws SpreadsheetReadException { 91 | // Excel Reader 92 | LOGGER.info("test_read_xlsx_single_sheet :: Reading file - {}", _filepath_xlsx_competition); 93 | XlsxReader reader = new XlsxReader(); 94 | 95 | // Read 96 | List employees = reader.read(CompetitionData.class, new File(_filepath_xlsx_competition)); 97 | Assert.assertNotNull(employees); 98 | Assert.assertTrue(employees.size() > 0); 99 | 100 | // LOGGER.info("Done"); 101 | for (CompetitionData emp : employees) { 102 | LOGGER.info(":: Output - {}", emp); 103 | } 104 | } 105 | 106 | 107 | @Test 108 | public void test_read_xlsx_multiple_sheets() throws SpreadsheetReadException { 109 | // Excel Reader 110 | LOGGER.info("test_read_xlsx_multiple_sheets :: Reading file - {}", _filepath_xlsx_multiple_sheets); 111 | XlsxReader ger = new XlsxReader(); 112 | 113 | // Read Sheet 1 114 | List employees = ger.read(Employee.class, new File(_filepath_xlsx_multiple_sheets), 0); 115 | Assert.assertNotNull(employees); 116 | Assert.assertTrue(employees.size() > 0); 117 | 118 | for (Employee emp : employees) { 119 | LOGGER.info("test_read_xlsx_multiple_sheets :: Output - {}", emp); 120 | } 121 | 122 | // Read Sheet 2 123 | List companies = ger.read(Company.class, new File(_filepath_xlsx_multiple_sheets), 1); 124 | Assert.assertNotNull(companies); 125 | Assert.assertTrue(companies.size() > 0); 126 | 127 | for (Company company : companies) { 128 | LOGGER.info("test_read_xlsx_multiple_sheets :: Output - {}", company); 129 | } 130 | } 131 | 132 | 133 | 134 | // Read from Stream 135 | 136 | @Test 137 | public void test_read_xlsx_single_sheet_from_stream() throws SpreadsheetReadException, FileNotFoundException { 138 | // Excel Reader 139 | LOGGER.info("test_read_xlsx_single_sheet_from_stream :: Reading file - {}", _filepath_xlsx_single_sheet); 140 | XlsxReader reader = new XlsxReader(); 141 | 142 | // InputStream 143 | final InputStream fis = new FileInputStream(new File(_filepath_xlsx_single_sheet)); 144 | 145 | // Read 146 | List employees = reader.read(Employee.class, fis); 147 | Assert.assertNotNull(employees); 148 | Assert.assertTrue(employees.size() > 0); 149 | 150 | for (Employee emp : employees) { 151 | LOGGER.info("test_read_xlsx_single_sheet :: Output - {}", emp); 152 | } 153 | } 154 | 155 | @Test 156 | public void test_read_xlsx_multiple_sheets_from_stream() throws SpreadsheetReadException, FileNotFoundException { 157 | // Excel Reader 158 | LOGGER.info("test_read_xlsx_multiple_sheets_from_stream :: Reading file - {}", _filepath_xlsx_multiple_sheets); 159 | XlsxReader reader = new XlsxReader(); 160 | 161 | // InputStream 162 | final InputStream fisSheet1 = new FileInputStream(new File(_filepath_xlsx_multiple_sheets)); 163 | 164 | // Read Sheet 1 165 | List employees = reader.read(Employee.class, fisSheet1, 0); 166 | Assert.assertNotNull(employees); 167 | Assert.assertTrue(employees.size() > 0); 168 | 169 | for (Employee emp : employees) { 170 | LOGGER.info("test_read_xlsx_multiple_sheets :: Output - {}", emp); 171 | } 172 | 173 | // InputStream 174 | final InputStream fisSheet2 = new FileInputStream(new File(_filepath_xlsx_multiple_sheets)); 175 | 176 | // Read Sheet 2 177 | List companies = reader.read(Company.class, fisSheet2, 1); 178 | Assert.assertNotNull(companies); 179 | Assert.assertTrue(companies.size() > 0); 180 | 181 | for (Company company : companies) { 182 | LOGGER.info("test_read_xlsx_multiple_sheets :: Output - {}", company); 183 | } 184 | } 185 | 186 | 187 | // Read with Callback 188 | 189 | @Test 190 | public void test_read_xlsx_single_sheet_with_callback() throws SpreadsheetReadException { 191 | // Excel Reader 192 | LOGGER.info("test_read_xlsx_single_sheet_with_callback :: Reading file - {}", _filepath_xlsx_single_sheet); 193 | 194 | // file 195 | final File xlsxFile = new File(_filepath_xlsx_single_sheet); 196 | 197 | final List employees = new ArrayList(); 198 | 199 | // Read 200 | XlsxReader reader = new XlsxReader(); 201 | reader.read(Employee.class, xlsxFile, new RowListener() { 202 | 203 | @Override 204 | public void row(int rowNum, Employee employee) { 205 | employees.add(employee); 206 | LOGGER.info("test_read_xlsx_single_sheet_with_callback :: Output - {}", employee); 207 | 208 | } 209 | }); 210 | 211 | Assert.assertNotNull(employees); 212 | Assert.assertTrue(employees.size() > 0); 213 | } 214 | 215 | 216 | 217 | // Read to Map 218 | 219 | 220 | @Test 221 | public void test_read_xlsx_as_Map() throws FileNotFoundException { 222 | // Excel Reader 223 | LOGGER.info("test_read_xlsx_as_Map :: Reading file - {}", _filepath_xlsx_single_sheet); 224 | XlsxReader ger = new XlsxReader(); 225 | 226 | // Read 227 | /* 228 | List> employees = ger.readAsMap(new File(_filepath_xlsx_single_sheet), 1); 229 | Assert.assertNotNull(employees); 230 | Assert.assertTrue(employees.size() > 0); 231 | 232 | for (Map emp : employees) { 233 | LOGGER.info("test_read_xlsx_single_sheet :: Output - {}", emp); 234 | } 235 | */ 236 | } 237 | 238 | 239 | } 240 | --------------------------------------------------------------------------------