├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── core ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── creditdatamw │ │ └── zerocell │ │ ├── Reader.java │ │ ├── ReaderUtil.java │ │ ├── SheetNotFoundException.java │ │ ├── ZeroCellException.java │ │ ├── ZeroCellReader.java │ │ ├── annotation │ │ ├── Column.java │ │ ├── RowNumber.java │ │ └── ZerocellReaderBuilder.java │ │ ├── column │ │ ├── ColumnInfo.java │ │ ├── ColumnMapping.java │ │ └── RowNumberInfo.java │ │ ├── converter │ │ ├── BooleanConverter.java │ │ ├── Converter.java │ │ ├── ConverterUtils.java │ │ ├── Converters.java │ │ ├── DefaultConverter.java │ │ ├── DefaultConverters.java │ │ ├── DoubleConverter.java │ │ ├── FallbackStrategy.java │ │ ├── FloatConverter.java │ │ ├── IntegerConverter.java │ │ ├── LocalDateConverter.java │ │ ├── LocalDateTimeConverter.java │ │ ├── LongConverter.java │ │ ├── NoopConverter.java │ │ ├── SqlDateConverter.java │ │ └── SqlTimestampConverter.java │ │ ├── handler │ │ ├── EntityExcelSheetHandler.java │ │ └── EntityHandler.java │ │ └── internal │ │ ├── IgnoreInvalidValueException.java │ │ └── package-info.java │ └── test │ ├── java │ └── com │ │ └── creditdatamw │ │ └── zerocell │ │ ├── Person.java │ │ ├── TestHandler.java │ │ ├── TestProgrammaticApi.java │ │ ├── TestReader.java │ │ ├── TestReaderUtil.java │ │ ├── TestSkipEmptyRows.java │ │ ├── column │ │ ├── TestColumnInfo.java │ │ ├── TestColumnMapping.java │ │ └── TestRowNumberInfo.java │ │ ├── converter │ │ ├── TestBooleanConverter.java │ │ ├── TestConverterUtils.java │ │ ├── TestDoubleConverter.java │ │ └── TestLongConverter.java │ │ └── handler │ │ ├── TestColumnFieldWriter.java │ │ ├── TestEntityExcelSheetHandler.java │ │ └── TestEntityHandler.java │ └── resources │ ├── test_people.xlsx │ ├── test_people_with_empty_rows.xlsx │ ├── test_people_with_offset_header.xlsx │ └── test_people_with_offset_sheet.xlsx ├── example ├── pom.xml └── src │ ├── generated │ └── java │ │ └── com │ │ └── creditdatamw │ │ └── zerocell │ │ └── example │ │ └── PersonExcelReader.java │ └── main │ └── java │ └── com │ └── creditdatamw │ └── zerocell │ └── example │ ├── AnnotationProcessorExample.java │ ├── BasicUsageExample.java │ ├── IdPrefixingConverter.java │ ├── ManualColumnMappingExample.java │ ├── Patient.java │ └── Person.java ├── findbugs-exclude.xml ├── pom.xml └── processor ├── pom.xml └── src └── main └── java └── com └── creditdatamw └── zerocell └── processor ├── ZeroCellAnnotationProcessor.java └── spec ├── CellMethodSpec.java ├── ColumnInfoType.java └── ReaderTypeSpec.java /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .classpath 3 | .idea 4 | .project 5 | .settings 6 | *.iml 7 | *.ipr 8 | *.iws 9 | *.log 10 | *.pyc 11 | *.swp 12 | *~ 13 | logs 14 | node_modules 15 | target 16 | apidoc 17 | template_schemas 18 | dependency-reduced-pom.xml 19 | notes 20 | backups/ 21 | pom.xml.versionsBackup -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to get contributions from you! 4 | 5 | If you have any questions or run into any problems, please create 6 | an issue here or email us: it [at] creditdatamw.com 7 | 8 | ## Workflow 9 | 10 | The supported workflow for contributing is as follows: 11 | 12 | 1. Fork the repo 13 | 1. Make a feature or bugfix branch (use `git checkout -b "your-feature-or-fix"`) 14 | 1. Make your cool new feature or bugfix on your branch 15 | 1. Include tests for your change 16 | 1. From your branch, make a pull request 17 | 1. The maintainers will review your change 18 | 1. Once your change has been reviewed and accepted, wait for your change to be merged into `master` 19 | 1. Delete your feature branch 20 | 21 | ## Style 22 | 23 | We use [SpotBugs][spotbugs], which uses static analysis to look for bugs 24 | in Java code, to catch common bugs and we generally follow a style guide 25 | similar to the [Google Java Style Guide][style] with exception on 26 | indentation (we use 4 spaces) 27 | 28 | ## Issues & Pull Requests 29 | 30 | When creating an issue please explain the steps to reproduce 31 | the issue or include a code snippet to illustrate the issue in more 32 | detail. The maintainers will review all issues created and we'll do 33 | our best to give feedback if necessary. However, we recommend that you send 34 | a Pull Request with a fix if you can manage - contributions are always welcome 35 | 36 | As outlined above, you must create a feature or bugfix branch and make a 37 | pull request from the branch which the maintainers will review. 38 | You may be asked to improve some things in the pull request and once the 39 | pull request is satisfactory it will be merged into the mainline. 40 | 41 | Please see the following post on writing [good commit messages](https://chris.beams.io/posts/git-commit/) 42 | 43 | ## Code Review 44 | 45 | The repository on GitHub is kept in sync with an internal repository at 46 | Credit Data CRB Ltd. For the most part this process should be transparent 47 | but it may have some implications on how pull requests are merged in. 48 | 49 | When you submit a pull request on GitHub, it will be reviewed 50 | inside (and possibly outside of) Credit Data CRB Ltd, and once the changes are 51 | approved, your commits will be brought into the internal system for additional 52 | testing. Once the changes are merged internally, they will be pushed back to 53 | GitHub. 54 | 55 | [spotbugs]: https://spotbugs.github.io/ 56 | [style]: https://google.github.io/styleguide/javaguide.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Credit Data CRB Ltd 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/github/license/creditdatamw/zerocell.svg)](./LICENSE) 2 | [![](https://img.shields.io/maven-central/v/com.creditdatamw.labs/zerocell-core.svg)](http://mvnrepository.com/artifact/com.creditdatamw.labs/zerocell-core) 3 | 4 | ZeroCell 5 | ======== 6 | 7 | ZeroCell provides a simple API for loading data from Excel sheets into 8 | Plain Old Java Objects (POJOs) using annotations to map columns from an Excel sheet 9 | to fields in Java classes. 10 | 11 | In case you don't fancy annotations or don't want to have to change your existing classes, 12 | you can map the columns to the fields without the annotations. 13 | 14 | ## Why should I use this? 15 | 16 | The library doesn't use the same approach that Apache POIs usermodel API and 17 | other POI based libraries use to process/store data loaded from the Excel file 18 | as a result it uses less resources as it doesn't process things such as Cell styles that take up memory. 19 | You also don't have to spend time setting data from cells to your Java objects, just 20 | define the mappings and let ZeroCell handle the rest. 21 | 22 | ## What ZeroCell _cannot_ do for you 23 | 24 | * Read or process excel workbook styles and other visual effects 25 | * Load data into complex object hierarchies 26 | * Write to excel files: The Apache POI library (which we use underneath) has a good API for writing to Excel files and provides the `SXSSFWorkbook` for writing large files in an efficient manner. 27 | 28 | ## Usage 29 | 30 | There are three ways to use zerocell: via annotations, the programmatic api and using the annotation processor. 31 | 32 | First things first, add the following dependency to your `pom.xml` 33 | 34 | ```xml 35 | 36 | com.creditdatamw.labs 37 | zerocell-core 38 | 0.3.2 39 | 40 | ``` 41 | 42 | ### Using Annotations 43 | 44 | You create a class with `@Column` (and optionally `@RowNumber`) 45 | annotations to represent a row in an Excel sheet and 46 | then use the static methods on the `Reader` class to read the 47 | list of data from the file. 48 | 49 | For example: 50 | 51 | ```java 52 | public class Person { 53 | @RowNumber 54 | private int rowNumber; 55 | 56 | @Column(index=0, name="FIRST_NAME") 57 | private String firstName; 58 | 59 | @Column(index=1, name="LAST_NAME") 60 | private String lastName; 61 | 62 | @Column(index=2, name="DATE_OF_BIRTH") 63 | private LocalDate dateOfBirth; 64 | 65 | // Getters and setters here ... 66 | 67 | public static void main(String... args) { 68 | // Then using the `Reader` class you can load 69 | // a list from the excel file as follows: 70 | List people = Reader.of(Person.class) 71 | .from(new File("people.xlsx")) 72 | .sheet("Sheet 1") 73 | .list(); 74 | 75 | // You can also inspect the column names of 76 | // the class using the static `columnsOf` method: 77 | String[] columns = Reader.columnsOf(Person.class); 78 | } 79 | } 80 | ``` 81 | 82 | ### Using the Programmatic API 83 | 84 | If you don't want to use annotations you can still use ZeroCell to load from Excel sheet 85 | to your Java objects without too much work. The only difference with the annotation approach 86 | is that you have to define the column mappings via the `Reader.using` method. 87 | 88 | For example: 89 | 90 | ```java 91 | public class Person { 92 | private int rowNo; 93 | 94 | private String id; 95 | 96 | private String firstName; 97 | 98 | private String middleName; 99 | 100 | private String lastName; 101 | 102 | private LocalDate dateOfBirth; 103 | 104 | private LocalDate dateOfRegistration; 105 | 106 | // Getters and setters here ... 107 | 108 | public static void main(String... args) { 109 | // Map the columns using, Reader.using method here 110 | List people = Reader.of(Person.class) 111 | .from(new File("people.xlsx")) 112 | .using( 113 | new RowNumberInfo("rowNo", Integer.class), 114 | new ColumnInfo("ID", "id", 0, String.class), 115 | new ColumnInfo("FIRST_NAME", "firstName", 1, String.class), 116 | new ColumnInfo("MIDDLE_NAME", "middleName", 2, String.class), 117 | new ColumnInfo("LAST_NAME", "lastName", 3, String.class), 118 | new ColumnInfo("DATE_OF_BIRTH", "dateOfBirth", 4, LocalDate.class), 119 | new ColumnInfo("DATE_REGISTERED", "dateOfRegistration", 5, Date.class) 120 | ) 121 | .sheet("Sheet 1") 122 | .list(); 123 | 124 | people.forEach(person -> { 125 | // Do something with person here 126 | }); 127 | } 128 | } 129 | ``` 130 | 131 | ### Using the Annotation Processor 132 | 133 | ZeroCell provides an annotation processor to generate Reader 134 | classes to read records from Excel without Runtime reflection 135 | which makes the code amenable to better auditing and customization. 136 | 137 | In order to use the functionality you will _need_ to add 138 | the `zerocell-processor` dependency to your POM. This adds a compile-time 139 | annotation processor which generates the classes: 140 | 141 | ```xml 142 | 143 | com.creditdatamw.labs 144 | zerocell-processor 145 | 0.3.2 146 | provided 147 | 148 | ``` 149 | 150 | Then, in your code use the `@ZerocellReaderBuilder` annotation on a class 151 | that contains ZeroCell `@Column` annotations. 152 | 153 | Using a class defined as in the example shown below: 154 | 155 | ```java 156 | package com.example; 157 | 158 | @ZerocellReaderBuilder 159 | public class Person { 160 | @RowNumber 161 | private int rowNumber; 162 | 163 | @Column(index=0, name="FIRST_NAME") 164 | private String firstName; 165 | 166 | @Column(index=1, name="LAST_NAME") 167 | private String lastName; 168 | 169 | @Column(index=2, name="DATE_OF_BIRTH") 170 | private LocalDate dateOfBirth; 171 | 172 | public static void main(String... args) { 173 | File file = new File("people.xlsx"); 174 | String sheet = "Sheet 1"; 175 | ZeroCellReader reader = new com.example.PersonReader(); 176 | List people = reader.read(file, sheet); 177 | people.forEach(person -> { 178 | // do something with person 179 | }); 180 | } 181 | } 182 | ``` 183 | 184 | Generates a class in the com.example package 185 | 186 | ```java 187 | package com.example; 188 | 189 | public class PersonReader implements ZeroCellReader { 190 | // generated code here 191 | } 192 | ``` 193 | 194 | 195 | ## Using Converters 196 | 197 | Converters allow you to process the value loaded from an Excel Cell for a 198 | particular field, primarily converters enable you to transform String values to 199 | another data type. This allows you to load data into fields that have types 200 | other than the default supported types. 201 | 202 | An example converter is shown below: 203 | 204 | ```java 205 | public class SimpleISOCurrency { 206 | public final String isoCurrency; 207 | public final double amount; 208 | 209 | public SimpleISOCurrency(String iso, double amount) { 210 | assert amount > 0.0; 211 | this.isoCurrency = iso; 212 | this.amount = amount; 213 | } 214 | } 215 | 216 | public class MalawiKwachaConverter implements Converter { 217 | @Override 218 | public SimpleISOCurrency convert(String value, String columnName, int row) { 219 | return new SimpleISOCurrency("MWK", Double.parseDouble(value)); 220 | } 221 | } 222 | 223 | // Usage looks like this: 224 | 225 | // ... 226 | @Column(index=1, name="Balance (MWK)", converter=MalawiKwachaConverter.class) 227 | private SimpleISOCurrency balance; 228 | // ... 229 | ``` 230 | 231 | ### Using converters for pre-processing 232 | 233 | You can also use converters as sort of pre-processing step where you operate on 234 | the String from the file before it's set on the field. 235 | 236 | Below is a simple example: 237 | 238 | ```java 239 | /** 240 | * Simple Converter that prefixes values with ID- 241 | */ 242 | public class IdPrefixingConverter implements Converter { 243 | @Override 244 | public String convert(String value, String columnName, int row) { 245 | return String.format("ID-%s", value); 246 | } 247 | } 248 | 249 | // Usage looks like this: 250 | 251 | // ... 252 | @Column(index=3, name="ID No.", converter=IdPrefixingConverter.class) 253 | private String idNo; 254 | // ... 255 | ``` 256 | 257 | ### Basic ISO LocalDate Converter 258 | 259 | Below is a simple implementation of an converter for `java.time.LocalDate` that 260 | you can use. 261 | 262 | > Please note: that if you need to parse date times considering timezones 263 | > you should implement your own converter and use a type like 264 | > [OffsetDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html) 265 | 266 | ```java 267 | 268 | public class BasicISOLocalDateConverter implements Converter { 269 | /** 270 | * Basic ISO converter - attempts to parse a string that is formatted as an 271 | * ISO8601 date and convert it to a java.time.LocalDate instance. 272 | * 273 | * @param value the value to convert 274 | * @param column the name of the current column 275 | * @param row the current row index 276 | * 277 | * @throws ZeroCellException if the value cannot be parsed as an ISO date 278 | */ 279 | @Override 280 | public LocalDate convert(String value, String column, int row) { 281 | if (value == null) return null; 282 | DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE; 283 | try { 284 | return LocalDate.parse(value, formatter); 285 | } catch (DateTimeParseException e) { 286 | throw new ZeroCellException(e); 287 | } 288 | } 289 | } 290 | ``` 291 | 292 | ## Loading the Correct Sheet 293 | 294 | If you do not specify the name of the sheet to load from, zerocell attempts to 295 | load data from the first Sheet in the Workbook. If the Sheet doesn't have matching 296 | columns as the defined model an exception will be thrown. In order to ensure 297 | the correct sheet is loaded it is recommended to always specify the sheet name. 298 | 299 | ## Exception Handling 300 | 301 | The API throws `ZeroCellException` if something goes wrong, e.g. sheet not found. 302 | It is an unchecked exception and may cause your code to stop executing if not 303 | handled. Typically `ZeroCellException` will wrap another exception, so it's worth 304 | peeking at the cause using `Exception#getCause`. 305 | 306 | ## CONTRIBUTING 307 | 308 | See the [`CONTRIBUTING.md`](CONTRIBUTING.md) file for more information. 309 | 310 | --- 311 | 312 | Copyright (c) Credit Data CRB Ltd 313 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zerocell-parent 5 | com.creditdatamw.labs 6 | 7 | 0.5.2-SNAPSHOT 8 | 9 | 10 | 4.0.0 11 | 12 | zerocell-core 13 | 14 | 15 | 16 | org.apache.commons 17 | commons-lang3 18 | 3.4 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/Reader.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.column.ColumnInfo; 4 | import com.creditdatamw.zerocell.column.ColumnMapping; 5 | import com.creditdatamw.zerocell.column.RowNumberInfo; 6 | import com.creditdatamw.zerocell.handler.EntityHandler; 7 | 8 | import java.io.File; 9 | import java.io.InputStream; 10 | import java.util.List; 11 | import java.util.Objects; 12 | 13 | /** 14 | * Main API for ZeroCell 15 | */ 16 | public class Reader { 17 | 18 | public static String[] columnsOf(Class clazz) { 19 | return ColumnInfo.columnsOf(clazz); 20 | } 21 | 22 | public static ReaderBuilder of(Class clazz) { 23 | return new ReaderBuilder<>(clazz); 24 | } 25 | 26 | public static final class ReaderBuilder { 27 | private final Class clazz; 28 | private File file; 29 | private String sheetName; 30 | private ColumnMapping columnMapping; 31 | private boolean skipHeaderRow = false; 32 | private boolean skipEmptyRows = true; 33 | private int skipFirstNRows = 0; 34 | private int maxRowNumber = 0; 35 | private InputStream inputStream; 36 | 37 | public ReaderBuilder(Class clazz) { 38 | this.clazz = clazz; 39 | } 40 | 41 | public ReaderBuilder from(File file) { 42 | Objects.requireNonNull(file); 43 | this.file = file; 44 | return this; 45 | } 46 | 47 | public ReaderBuilder from(InputStream inputStream) { 48 | Objects.requireNonNull(inputStream); 49 | this.inputStream = inputStream; 50 | return this; 51 | } 52 | 53 | public ReaderBuilder using(RowNumberInfo rowNumberInfo, ColumnInfo... columns) { 54 | this.columnMapping = new ColumnMapping(rowNumberInfo, columns); 55 | return this; 56 | } 57 | 58 | public ReaderBuilder using(ColumnInfo... columns) { 59 | this.columnMapping = new ColumnMapping(null, columns); 60 | return this; 61 | } 62 | 63 | public ReaderBuilder sheet(String sheetName) { 64 | Objects.requireNonNull(sheetName); 65 | this.sheetName = sheetName; 66 | return this; 67 | } 68 | 69 | public ReaderBuilder skipHeaderRow(boolean value) { 70 | this.skipHeaderRow = value; 71 | return this; 72 | } 73 | 74 | public ReaderBuilder skipEmptyRows(boolean value) { 75 | this.skipEmptyRows = value; 76 | return this; 77 | } 78 | 79 | /** 80 | * Set the number of rows to skip before the header 81 | * 82 | * @param value is the number of rows to skip 83 | * @return ReaderBuilder 84 | */ 85 | public ReaderBuilder skipFirstNRows(int value) { 86 | assert (value > 0); 87 | this.skipFirstNRows = value; 88 | this.maxRowNumber += value; 89 | return this; 90 | } 91 | 92 | /** 93 | * Set the maximum number of rows to parse. 94 | * 95 | * @param rowNumber the number of rows to parse. 96 | * @return ReaderBuilder 97 | */ 98 | public ReaderBuilder setRowNumber(int rowNumber) { 99 | assert (rowNumber > 0); 100 | this.maxRowNumber += rowNumber; 101 | return this; 102 | } 103 | 104 | @SuppressWarnings("unchecked") 105 | public List list() { 106 | EntityHandler entityHandler; 107 | if (!Objects.isNull(sheetName) && !Objects.isNull(columnMapping)) { 108 | entityHandler = new EntityHandler(clazz, sheetName, columnMapping, skipHeaderRow, skipFirstNRows, maxRowNumber); 109 | } else if (Objects.isNull(sheetName) && !Objects.isNull(columnMapping)) { 110 | entityHandler = new EntityHandler(clazz, columnMapping, skipHeaderRow, skipFirstNRows, maxRowNumber); 111 | } else if (!Objects.isNull(sheetName) && Objects.isNull(columnMapping)) { 112 | entityHandler = new EntityHandler(clazz, sheetName, skipHeaderRow, skipFirstNRows, maxRowNumber); 113 | } else { 114 | entityHandler = new EntityHandler(clazz, skipHeaderRow, skipFirstNRows, maxRowNumber); 115 | } 116 | entityHandler.setSkipEmptyRows(this.skipEmptyRows); 117 | 118 | if (Objects.nonNull(file)) { 119 | entityHandler.process(file); 120 | } else { 121 | entityHandler.process(inputStream); 122 | } 123 | 124 | return entityHandler.readAsList(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/ReaderUtil.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.handler.EntityHandler; 4 | import org.apache.poi.EmptyFileException; 5 | import org.apache.poi.openxml4j.exceptions.InvalidFormatException; 6 | import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException; 7 | import org.apache.poi.openxml4j.opc.OPCPackage; 8 | import org.apache.poi.openxml4j.opc.PackageAccess; 9 | import org.apache.poi.ss.usermodel.DataFormatter; 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.model.StylesTable; 15 | import org.xml.sax.InputSource; 16 | import org.xml.sax.XMLReader; 17 | 18 | import java.io.*; 19 | import java.util.Objects; 20 | 21 | /** 22 | * Utility class for Reader implementations 23 | * 24 | */ 25 | public final class ReaderUtil { 26 | public static final String ERROR_NOT_OPENXML = "Cannot load file. The file must be an Excel 2007+ Workbook (.xlsx)"; 27 | /** 28 | * Reads a list of POJOs from the given excel file. 29 | * 30 | * @param path Excel file to read from 31 | * @param sheetName The sheet to extract from in the workbook 32 | * @param reader The reader class to use to load the file from the sheet 33 | */ 34 | public static void process(String path, String sheetName, ZeroCellReader reader) { 35 | if (path == null || path.trim().isEmpty()) { 36 | throw new IllegalArgumentException("'path' must be given"); 37 | } 38 | 39 | File file = new File(path); 40 | if (file.exists() && file.isDirectory()) { 41 | throw new IllegalArgumentException("path must not be a directory"); 42 | } 43 | try (OPCPackage opcPackage = OPCPackage.open(file.getAbsolutePath(), PackageAccess.READ)) { 44 | process(opcPackage, sheetName, reader); 45 | } catch(InvalidFormatException | EmptyFileException | NotOfficeXmlFileException ife) { 46 | throw new ZeroCellException(ERROR_NOT_OPENXML); 47 | } catch (IOException ioe) { 48 | throw new ZeroCellException("Failed to process file", ioe); 49 | } 50 | } 51 | 52 | /** 53 | * Reads a list of POJOs from the given excel file. 54 | * 55 | * @param file Excel file to read from 56 | * @param sheetName The sheet to extract from in the workbook 57 | * @param reader The reader class to use to load the file from the sheet 58 | */ 59 | public static void process(File file, String sheetName, ZeroCellReader reader) { 60 | try (OPCPackage opcPackage = OPCPackage.open(file, PackageAccess.READ)) { 61 | process(opcPackage, sheetName, reader); 62 | } catch(InvalidFormatException | EmptyFileException | NotOfficeXmlFileException ife) { 63 | throw new ZeroCellException(ERROR_NOT_OPENXML); 64 | } catch (IOException ioe) { 65 | throw new ZeroCellException("Failed to process file", ioe); 66 | } 67 | } 68 | 69 | /** 70 | * Reads a list of POJOs from the given input stream. 71 | * 72 | * @param is InputStream to read Excel file from 73 | * @param sheetName The sheet to extract from in the workbook 74 | * @param reader The reader class to use to load the file from the sheet 75 | */ 76 | public static void process(InputStream is, String sheetName, ZeroCellReader reader) { 77 | try (PushbackInputStream p = new PushbackInputStream(is, 16); 78 | OPCPackage opcPackage = OPCPackage.open(p)) { 79 | process(opcPackage, sheetName, reader); 80 | } catch (Exception e) { 81 | throw new ZeroCellException("Failed to process file", e); 82 | } 83 | } 84 | 85 | /** 86 | * Processes data from an Excel file contained in the OPCPackage using the 87 | * reader implementation 88 | *

89 | * Please note that the process will read data from the first sheet in the 90 | * File when if sheet name is not specified 91 | * (i.e. the sheet name defaults to the {@link EntityHandler.DEFAULT_SHEET}) 92 | *

93 | * @param opcPackage the OpenXML OPC Package 94 | * @param sheetName The sheet name 95 | * @param reader the reader implementation that handles the entity mapping 96 | */ 97 | private static void process(OPCPackage opcPackage, String sheetName, ZeroCellReader reader) { 98 | try { 99 | DataFormatter dataFormatter = new DataFormatter(); 100 | ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(opcPackage); 101 | XSSFReader xssfReader = new XSSFReader(opcPackage); 102 | StylesTable stylesTable = xssfReader.getStylesTable(); 103 | InputStream sheetInputStream = null; 104 | XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) xssfReader.getSheetsData(); 105 | while (sheets.hasNext()) { 106 | sheetInputStream = sheets.next(); 107 | 108 | if (EntityHandler.DEFAULT_SHEET.equalsIgnoreCase(sheetName)) { 109 | break; 110 | } 111 | if (sheets.getSheetName().equalsIgnoreCase(sheetName)) { 112 | break; 113 | } else { 114 | sheetInputStream = null; 115 | } 116 | } 117 | 118 | if (Objects.isNull(sheetInputStream)) { 119 | throw new SheetNotFoundException(sheetName); 120 | } 121 | 122 | XMLReader xmlReader = XMLHelper.newXMLReader(); 123 | xmlReader.setContentHandler(new XSSFSheetXMLHandler(stylesTable, strings, reader, dataFormatter, false)); 124 | xmlReader.parse(new InputSource(sheetInputStream)); 125 | sheetInputStream.close(); 126 | xmlReader = null; 127 | sheetInputStream = null; 128 | stylesTable = null; 129 | strings = null; 130 | xssfReader = null; 131 | } catch(InvalidFormatException | EmptyFileException | NotOfficeXmlFileException ife) { 132 | throw new ZeroCellException(ERROR_NOT_OPENXML); 133 | } catch(SheetNotFoundException ex) { 134 | throw new ZeroCellException(ex.getMessage()); 135 | } catch (ZeroCellException ze) { 136 | throw ze; // Rethrow the Exception 137 | } catch (Exception e) { 138 | throw new ZeroCellException("Failed to process file", e); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/SheetNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | public class SheetNotFoundException extends Exception { 4 | public SheetNotFoundException(String message) { 5 | super(String.format("Could not find sheet %s", message)); 6 | } 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/ZeroCellException.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | /** 4 | * RuntimeException occurred 5 | */ 6 | public class ZeroCellException extends RuntimeException { 7 | public ZeroCellException(String message) { 8 | super(message); 9 | } 10 | 11 | public ZeroCellException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ZeroCellException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public ZeroCellException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/ZeroCellReader.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler; 4 | 5 | import java.io.File; 6 | import java.util.List; 7 | 8 | /** 9 | * Proxy for ZeroCellReader Implementations 10 | */ 11 | public interface ZeroCellReader extends XSSFSheetXMLHandler.SheetContentsHandler { 12 | List read(final File file, final String sheet); 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/annotation/Column.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.annotation; 2 | 3 | import com.creditdatamw.zerocell.converter.FallbackStrategy; 4 | import com.creditdatamw.zerocell.converter.NoopConverter; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * A Column in the excel workbook 13 | */ 14 | @Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface Column { 17 | /** 18 | * 19 | * @return Name of the column in the excelWorkbook - this must be the value in that column 20 | */ 21 | String name(); 22 | 23 | /** 24 | * 25 | * @return Position of the column. Starts with the 0 index. 26 | */ 27 | int index(); 28 | 29 | /** 30 | * 31 | * @return Format for the value of the column, iff applicable 32 | */ 33 | String dataFormat() default ""; 34 | 35 | /** 36 | * 37 | * @return Format for the value of the column, iff applicable 38 | */ 39 | FallbackStrategy fallback() default FallbackStrategy.DEFAULT; 40 | 41 | /** 42 | * 43 | * @return Converter to use to convert the string value to the field type's value 44 | */ 45 | Class converterClass() default NoopConverter.class; 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/annotation/RowNumber.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.annotation; 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 | import static java.lang.annotation.ElementType.FIELD; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | /** 12 | * Keep track of the source row number in the excel using this annotation. 13 | * Apply it to an integer type to store the value there 14 | */ 15 | @Target({ElementType.FIELD}) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface RowNumber { 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/annotation/ZerocellReaderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Marker annotation for a class that Generates a ZeroCell Reader 7 | * 8 | */ 9 | @Retention(RetentionPolicy.SOURCE) 10 | @Target(ElementType.TYPE) 11 | @Documented 12 | public @interface ZerocellReaderBuilder { 13 | /** 14 | * Gets the name of the generated Reader class, which 15 | * defaults to the name of the class with suffix of "Reader" e.g. PersonReader 16 | * 17 | * @return The name of the generated Reader class. 18 | */ 19 | String value() default "__none__"; 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/column/ColumnInfo.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | import com.creditdatamw.zerocell.converter.FallbackStrategy; 4 | import com.creditdatamw.zerocell.annotation.Column; 5 | import com.creditdatamw.zerocell.converter.NoopConverter; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.lang.reflect.Field; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Objects; 12 | 13 | /** 14 | * Information about a Column in a Java Bean 15 | */ 16 | public class ColumnInfo { 17 | private String name; 18 | 19 | private String fieldName; 20 | 21 | private int index; 22 | 23 | private String dataFormat; 24 | 25 | private Class type; 26 | 27 | private Class converterClass; 28 | 29 | private FallbackStrategy fallbackStrategy; 30 | 31 | public ColumnInfo(String name, String fieldName, int index, Class dataType) { 32 | this(name, fieldName, index, "", dataType, NoopConverter.class); 33 | } 34 | 35 | public ColumnInfo(String name, String fieldName, int index, Class dataType, FallbackStrategy fallbackStrategy) { 36 | this(name, fieldName, index, "", dataType, NoopConverter.class, fallbackStrategy); 37 | } 38 | 39 | public ColumnInfo(String name, String fieldName, int index, String dataFormat, Class type, Class converterClass) { 40 | this(name, fieldName, index, dataFormat, type, converterClass, FallbackStrategy.DEFAULT); 41 | } 42 | 43 | public ColumnInfo(String name, String fieldName, int index, String dataFormat, Class type, Class converterClass, FallbackStrategy fallbackStrategy) { 44 | this.name = name; 45 | this.fieldName = fieldName; 46 | this.index = index; 47 | this.dataFormat = dataFormat; 48 | this.type = type; 49 | this.converterClass = converterClass; 50 | this.fallbackStrategy = fallbackStrategy; 51 | } 52 | 53 | /** 54 | * Name of the column in the Excel file 55 | * @return name of the column 56 | */ 57 | public String getName() { 58 | return name.toUpperCase().trim(); 59 | } 60 | 61 | /** 62 | * Name of the field/attribute in the class 63 | * @return name of the field in the class 64 | */ 65 | public String getFieldName() { 66 | return fieldName; 67 | } 68 | 69 | /** 70 | * Index of the column in the Excel file 71 | * @return column index 72 | */ 73 | public int getIndex() { 74 | return index; 75 | } 76 | 77 | /** 78 | * Data format specification. 79 | * @return data format specification 80 | */ 81 | public String getDataFormat() { 82 | return dataFormat; 83 | } 84 | 85 | /** 86 | * Class of the type of the data expected at that column in the Excel file 87 | * @return Class instance 88 | */ 89 | public Class getType() { 90 | return type; 91 | } 92 | 93 | public Class getConverterClass() { 94 | return converterClass; 95 | } 96 | 97 | public FallbackStrategy getFallbackStrategy() { 98 | return fallbackStrategy; 99 | } 100 | 101 | /** 102 | * Finds and extacts {@link Column} annotations from the provided class 103 | * 104 | * @param clazz the class to extraction annotations frlom 105 | * @return list of {@link Column} annotations from the clas 106 | */ 107 | public static final List annotationsOf(Class clazz) { 108 | Field[] fieldArray = clazz.getDeclaredFields(); 109 | List columns = new ArrayList<>(fieldArray.length); 110 | for (Field field : fieldArray) { 111 | Column annotation = field.getAnnotation(Column.class); 112 | if (!Objects.isNull(annotation)) { 113 | columns.add(annotation); 114 | } 115 | } 116 | return columns; 117 | } 118 | 119 | /** 120 | * Finds the column names for all fields annotated with {@link Column} 121 | * annotation. 122 | * 123 | * @param clazz the class to extract annotations from 124 | * @return array of column names 125 | */ 126 | public static final String[] columnsOf(Class clazz) { 127 | List columns = annotationsOf(clazz); 128 | String[] columnNames = new String[columns.size()]; 129 | 130 | LoggerFactory.getLogger(ColumnInfo.class).debug(String.format("Found %s columns in class %s", columnNames.length, clazz.getName())); 131 | 132 | for(Column annotation: columns) { 133 | columnNames[annotation.index()] = annotation.name().trim(); 134 | } 135 | columns = null; 136 | return columnNames; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/column/ColumnMapping.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | import com.creditdatamw.zerocell.ZeroCellException; 4 | import com.creditdatamw.zerocell.annotation.Column; 5 | import com.creditdatamw.zerocell.annotation.RowNumber; 6 | 7 | import java.lang.reflect.Field; 8 | import java.util.*; 9 | 10 | public final class ColumnMapping { 11 | private final RowNumberInfo rowNumberInfo; 12 | private final List columns; 13 | 14 | public ColumnMapping(RowNumberInfo rowNumberInfo, List columns) { 15 | this.rowNumberInfo = rowNumberInfo; 16 | this.columns = columns; 17 | } 18 | 19 | public ColumnMapping(RowNumberInfo rowNumberInfo,ColumnInfo... columns) { 20 | this.rowNumberInfo = rowNumberInfo; 21 | this.columns = Arrays.asList(columns); 22 | } 23 | 24 | /** 25 | * RowNumberInfo column if the {@link RowNumber} annotation is defined 26 | * on the class otherwise returns null 27 | * @return rown number info 28 | */ 29 | public RowNumberInfo getRowNumberInfo() { 30 | return rowNumberInfo; 31 | } 32 | 33 | /** 34 | * Columns defined in this mapping. The returned list is immutable 35 | * @return immutable list of ColumnInfo 36 | */ 37 | public List getColumns() { 38 | return Collections.unmodifiableList(columns); 39 | } 40 | 41 | /** 42 | * Creates a ColumnMapping from Zerocell {@link Column} 43 | * and {@link RowNumber} annotations applied on a Class 44 | * 45 | * @param clazz the class to extract column mapping from 46 | * @return column mapping of the class 47 | */ 48 | public static ColumnMapping parseColumnMappingFromAnnotations(Class clazz) { 49 | Field[] fieldArray = clazz.getDeclaredFields(); 50 | ArrayList list = new ArrayList<>(fieldArray.length); 51 | RowNumberInfo rowNumberColumn = null; 52 | for (Field field: fieldArray) { 53 | 54 | RowNumber rowNumberAnnotation = field.getAnnotation(RowNumber.class); 55 | 56 | if (! Objects.isNull(rowNumberAnnotation)) { 57 | rowNumberColumn = new RowNumberInfo(field.getName(), Integer.class); 58 | continue; 59 | } 60 | 61 | Column annotation = field.getAnnotation(Column.class); 62 | if (! Objects.isNull(annotation)) { 63 | Class converter = annotation.converterClass(); 64 | list.add(new ColumnInfo(annotation.name().trim(), 65 | field.getName(), 66 | annotation.index(), 67 | annotation.dataFormat(), 68 | field.getType(), 69 | converter, 70 | annotation.fallback())); 71 | } 72 | } 73 | 74 | if (list.isEmpty()) { 75 | throw new ZeroCellException(String.format("Class %s does not have @Column annotations", clazz.getName())); 76 | } 77 | list.trimToSize(); 78 | return new ColumnMapping(rowNumberColumn, list); 79 | } 80 | 81 | /** 82 | * Create th map, where key is the index from {@link Column} annotation, 83 | * and value is the ColumnInfo. 84 | * The method also checks the indexes duplicates and throws a 85 | * ZeroCellException in this case 86 | * 87 | * @return map of column info 88 | * @throws ZeroCellException in the case of a duplicate index 89 | */ 90 | public Map getColumnsMap() { 91 | Map map = new HashMap<>(); 92 | this.columns.forEach(info -> { 93 | int index = info.getIndex(); 94 | if (Objects.isNull(map.get(index))) { 95 | map.put(index, info); 96 | } else { 97 | throw new ZeroCellException("Cannot map two columns to the same index: " + index); 98 | } 99 | }); 100 | return map; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/column/RowNumberInfo.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | public final class RowNumberInfo extends ColumnInfo { 4 | public RowNumberInfo(String fieldName, Class dataType) { 5 | super("__rowNUmber__", fieldName, -1, dataType); 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/BooleanConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.ZeroCellException; 4 | 5 | public class BooleanConverter extends DefaultConverter { 6 | @Override 7 | public Boolean convert(String value, String columnName, int row) { 8 | try { 9 | return Boolean.valueOf(value); 10 | } catch(Exception e) { 11 | switch (this.getFallbackStrategy()) { 12 | case DEFAULT_TO_TRUE: 13 | return Boolean.TRUE; 14 | case LEGACY: 15 | case DEFAULT_TO_FALSE: 16 | return Boolean.FALSE; 17 | // This is a special case for DO_NOT_SET 18 | case IGNORE: // TODO(zikani03): Review whether this case is appropriate 19 | default: 20 | throw new ZeroCellException(String.format( 21 | "Failed to parse '%s' as Boolean at column='%s' row='%s'", value, columnName, row)); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/Converter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | /** 4 | * Converts a value from a String type to another 5 | */ 6 | @FunctionalInterface 7 | public interface Converter { 8 | T convert(String value, String columnName, int row); 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/ConverterUtils.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import java.sql.Timestamp; 4 | import java.time.LocalDate; 5 | import java.time.LocalDateTime; 6 | 7 | /** 8 | * Utility class for Converters 9 | */ 10 | public final class ConverterUtils { 11 | ConverterUtils() {} 12 | 13 | static final DefaultConverters defaultConverters = Converters.newDefaultConverters(); 14 | 15 | /** 16 | * Converts a value from a string to an object of the specified type 17 | * 18 | * @param fieldType Type of class to convert value to 19 | * @param formattedValue the value from Excel as a string 20 | * @param columnName The name of the column 21 | * @param rowNum The row number 22 | * @return a value converted from string to the corresponding type 23 | */ 24 | @Deprecated 25 | public static Object convertValueToType(Class fieldType, 26 | String formattedValue, 27 | String columnName, 28 | int rowNum) { 29 | 30 | Object value = null; 31 | if (fieldType == String.class) { 32 | value = String.valueOf(formattedValue); 33 | } else if (fieldType == LocalDateTime.class) { 34 | return Converters.toLocalDateTime 35 | .convert(formattedValue, columnName, rowNum); 36 | 37 | } else if (fieldType == LocalDate.class) { 38 | return Converters.toLocalDate 39 | .convert(formattedValue, columnName, rowNum); 40 | 41 | } else if (fieldType == java.sql.Date.class) { 42 | return Converters.toSqlDate 43 | .convert(formattedValue, columnName, rowNum); 44 | 45 | } else if (fieldType == Timestamp.class) { 46 | return defaultConverters.toSqlTimestamp 47 | .convert(formattedValue, columnName, rowNum); 48 | 49 | } else if (fieldType == Integer.class || fieldType == int.class) { 50 | return Converters.toInteger 51 | .convert(formattedValue, columnName, rowNum); 52 | 53 | } else if (fieldType == Long.class || fieldType == long.class) { 54 | return Converters.toLong 55 | .convert(formattedValue, columnName, rowNum); 56 | 57 | } else if (fieldType == Double.class || fieldType == double.class) { 58 | return Converters.toDouble 59 | .convert(formattedValue, columnName, rowNum); 60 | 61 | } else if (fieldType == Float.class || fieldType == float.class) { 62 | return defaultConverters.toFloat 63 | .convert(formattedValue, columnName, rowNum); 64 | 65 | } else if (fieldType == Boolean.class) { 66 | return defaultConverters.toBoolean 67 | .convert(formattedValue, columnName, rowNum); 68 | } 69 | 70 | return value; 71 | } 72 | 73 | /** 74 | * Converts a value from a string to an object of the specified type 75 | * 76 | * @param fieldType Type of class to convert value to 77 | * @param formattedValue the value from Excel as a string 78 | * @param columnName The name of the column 79 | * @param rowNum The row number 80 | * @return a value converted from string to the corresponding type 81 | */ 82 | public static Object convertValueToType(Class fieldType, 83 | String formattedValue, 84 | String columnName, 85 | int rowNum, 86 | FallbackStrategy fallbackStrategy) { 87 | 88 | 89 | Object value = null; 90 | if (fieldType == String.class) { 91 | value = String.valueOf(formattedValue); 92 | } else if (fieldType == LocalDateTime.class) { 93 | return defaultConverters.toLocalDateTime 94 | .withFallbackStrategy(fallbackStrategy) 95 | .convert(formattedValue, columnName, rowNum); 96 | 97 | } else if (fieldType == LocalDate.class) { 98 | return defaultConverters.toLocalDate 99 | .withFallbackStrategy(fallbackStrategy) 100 | .convert(formattedValue, columnName, rowNum); 101 | 102 | } else if (fieldType == java.sql.Date.class) { 103 | return defaultConverters.toSqlDate 104 | .withFallbackStrategy(fallbackStrategy) 105 | .convert(formattedValue, columnName, rowNum); 106 | 107 | } else if (fieldType == Timestamp.class) { 108 | return defaultConverters.toSqlTimestamp 109 | .withFallbackStrategy(fallbackStrategy) 110 | .convert(formattedValue, columnName, rowNum); 111 | 112 | } else if (fieldType == Integer.class || fieldType == int.class) { 113 | return defaultConverters.toInteger 114 | .withFallbackStrategy(fallbackStrategy) 115 | .convert(formattedValue, columnName, rowNum); 116 | 117 | } else if (fieldType == Long.class || fieldType == long.class) { 118 | return defaultConverters.toLong 119 | .withFallbackStrategy(fallbackStrategy) 120 | .convert(formattedValue, columnName, rowNum); 121 | 122 | } else if (fieldType == Double.class || fieldType == double.class) { 123 | return defaultConverters.toDouble 124 | .withFallbackStrategy(fallbackStrategy) 125 | .convert(formattedValue, columnName, rowNum); 126 | 127 | } else if (fieldType == Float.class || fieldType == float.class) { 128 | return defaultConverters.toFloat 129 | .withFallbackStrategy(fallbackStrategy) 130 | .convert(formattedValue, columnName, rowNum); 131 | 132 | } else if (fieldType == Boolean.class) { 133 | return defaultConverters.toBoolean 134 | .withFallbackStrategy(fallbackStrategy) 135 | .convert(formattedValue, columnName, rowNum); 136 | } 137 | 138 | return value; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/Converters.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.sql.Date; 7 | import java.sql.Timestamp; 8 | import java.time.LocalDate; 9 | import java.time.LocalDateTime; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Common Converters 14 | * 15 | * The converters will throw a ZeroCellException in case of failure to parse the 16 | * provided. 17 | */ 18 | public class Converters { 19 | private static final Logger LOGGER = LoggerFactory.getLogger(Converters.class); 20 | 21 | public static DefaultConverters newDefaultConverters() { 22 | return new DefaultConverters(); 23 | } 24 | 25 | public static final Converter toFloat = ((formattedValue, columnName, row) -> { 26 | try { 27 | return Float.valueOf(formattedValue); 28 | } catch (Exception e) { 29 | LOGGER.error("Failed to parse {} as Float. Using default of null at column={} row={} ", formattedValue, columnName, row); 30 | return null; 31 | } 32 | }); 33 | 34 | public static final Converter noop = ((formattedValue, columnName, row) -> { 35 | return formattedValue; 36 | }); 37 | 38 | public static final Converter toLocalDateTime = ((formattedValue, columnName, rowNum) -> { 39 | try { 40 | return LocalDateTime.parse(formattedValue); 41 | } catch(Exception e) { 42 | LOGGER.error("Failed to parse {} as Date. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 43 | return null; 44 | } 45 | }); 46 | 47 | 48 | public static final Converter toLocalDate = ((formattedValue, columnName, rowNum) -> { 49 | try { 50 | return LocalDate.parse(formattedValue); 51 | } catch(Exception e) { 52 | LOGGER.error("Failed to parse {} as Date. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 53 | return null; 54 | } 55 | }); 56 | 57 | public static final Converter toSqlDate = ((formattedValue, columnName, rowNum) -> { 58 | try { 59 | return Objects.isNull(formattedValue) ? null : Date.valueOf(formattedValue); 60 | } catch (Exception e) { 61 | LOGGER.error("Failed to parse {} as Date. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 62 | return null; 63 | } 64 | }); 65 | 66 | public static final Converter toSqlTimestamp = ((formattedValue, columnName, rowNum) -> { 67 | try { 68 | return Timestamp.valueOf(formattedValue); 69 | } catch (Exception e) { 70 | LOGGER.error("Failed to parse {} as Timestamp. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 71 | return null; 72 | } 73 | }); 74 | 75 | public static final Converter toInteger = ((formattedValue, columnName, rowNum) -> { 76 | try { 77 | return Integer.valueOf(formattedValue == null ? "0.0" : formattedValue); 78 | } catch (Exception e) { 79 | LOGGER.error("Failed to parse {} as Integer. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 80 | return null; 81 | } 82 | }); 83 | 84 | public static final Converter toLong = ((formattedValue, columnName, rowNum) -> { 85 | //} else if (fieldType == Long.class || fieldType == long.class) { 86 | try { 87 | return Long.valueOf(formattedValue); 88 | } catch (Exception e) { 89 | LOGGER.error("Failed to parse {} as Long. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 90 | return null; 91 | } 92 | }); 93 | 94 | public static final Converter toDouble = ((formattedValue, columnName, rowNum) -> { 95 | // } else if (fieldType == Double.class || fieldType == double.class) { 96 | try { 97 | return Double.valueOf(formattedValue); 98 | } catch (Exception e) { 99 | LOGGER.error("Failed to parse {} as Double. Using default of null at column={} row={} ", formattedValue, columnName, rowNum); 100 | return null; 101 | } 102 | }); 103 | 104 | public static final Converter toBoolean = ((formattedValue, columnName, rowNum) -> { 105 | try { 106 | return Boolean.valueOf(formattedValue == null ? "FALSE" : "TRUE"); 107 | } catch(Exception e) { 108 | LOGGER.error("Failed to parse {} as Boolean. Using default of false at column={} row={} ", formattedValue, columnName, rowNum); 109 | return Boolean.FALSE; 110 | } 111 | }); 112 | 113 | } 114 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/DefaultConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | 4 | abstract class DefaultConverter implements Converter { 5 | private FallbackStrategy fallbackStrategy; 6 | 7 | public DefaultConverter() { 8 | this.fallbackStrategy = FallbackStrategy.DEFAULT; 9 | } 10 | 11 | public DefaultConverter(FallbackStrategy fallbackStrategy) { 12 | this.fallbackStrategy = fallbackStrategy; 13 | } 14 | 15 | public DefaultConverter withFallbackStrategy(FallbackStrategy fallbackStrategy) { 16 | this.fallbackStrategy = fallbackStrategy; 17 | return this; 18 | } 19 | 20 | public FallbackStrategy getFallbackStrategy() { 21 | return fallbackStrategy; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/DefaultConverters.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import java.sql.Date; 4 | import java.sql.Timestamp; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | 8 | /** 9 | * Common Converters 10 | * 11 | * The converters will throw a ZeroCellException in case of failure to parse the 12 | * provided. 13 | */ 14 | public class DefaultConverters { 15 | DefaultConverters() {} 16 | 17 | public final DefaultConverter noop = new DefaultConverter(){ 18 | @Override 19 | public String convert(String value, String columnName, int row) { 20 | FallbackStrategy s = this.getFallbackStrategy(); 21 | 22 | if (value == null && 23 | (s == FallbackStrategy.DEFAULT_TO_EMPTY_STRING || 24 | s == FallbackStrategy.DEFAULT)) { 25 | 26 | return ""; 27 | } 28 | return value; 29 | } 30 | }; 31 | 32 | public final DefaultConverter toFloat = new FloatConverter() 33 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 34 | public final DefaultConverter toLocalDateTime = new LocalDateTimeConverter() 35 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 36 | public final DefaultConverter toLocalDate = new LocalDateConverter() 37 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 38 | public final DefaultConverter toSqlDate = new SqlDateConverter() 39 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 40 | public final DefaultConverter toSqlTimestamp = new SqlTimestampConverter() 41 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 42 | public final DefaultConverter toInteger = new IntegerConverter() 43 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 44 | public final DefaultConverter toLong = new LongConverter() 45 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 46 | public final DefaultConverter toDouble = new DoubleConverter() 47 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 48 | public final DefaultConverter toBoolean = new BooleanConverter() 49 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/DoubleConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | public class DoubleConverter extends DefaultConverter { 7 | private static final String message = "Failed to parse '%s' as Double at column='%s' row='%s'"; 8 | 9 | @Override 10 | public Double convert(String value, String columnName, int row) { 11 | try { 12 | return Double.parseDouble(value); 13 | } catch (Exception e) { 14 | switch (this.getFallbackStrategy()) { 15 | case LEGACY: 16 | case DEFAULT_TO_NULL: 17 | return null; 18 | case DEFAULT_TO_ZERO: 19 | return 0.0D; 20 | case THROW_EXCEPTION: 21 | throw new ZeroCellException(String.format(message, value, columnName, row)); 22 | case IGNORE: 23 | throw new IgnoreInvalidValueException(); 24 | case DEFAULT_TO_MIN_VALUE: 25 | case DEFAULT: 26 | default: 27 | return Double.MIN_VALUE; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/FallbackStrategy.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | public enum FallbackStrategy { 4 | /** 5 | * Uses the LEGACY approach for handling with failures, which is basically 6 | * setting the field to null for reference types and 7 | * 0 for numeric types. 8 | * 9 | * @deprecated this should be considered deprecated 10 | */ 11 | LEGACY, 12 | /** 13 | * Applies the default Fallback Strategy for the field. 14 | */ 15 | DEFAULT, 16 | /** 17 | * Ignores the invalid value from the Excel Cell - 18 | * does not set anything on the field. Basically skips the field. 19 | */ 20 | IGNORE, 21 | /** 22 | * Sets the field on the field to null. Regardless of type. 23 | */ 24 | DEFAULT_TO_NULL, 25 | /** 26 | * Sets the field to 0 if the value from the Excel cell 27 | * cannot be parsed as a numeric value. 28 | * Applies only to numeric types. 29 | */ 30 | DEFAULT_TO_ZERO, 31 | /** 32 | * Sets the field to true if the value from the Excel cell 33 | * cannot be parsed as a boolean value. 34 | * Applies only to Boolean types. 35 | */ 36 | DEFAULT_TO_TRUE, 37 | /** 38 | * Sets the field to false if the value from the Excel cell 39 | * cannot be parsed as a boolean value. 40 | * Applies only to Boolean types. 41 | */ 42 | DEFAULT_TO_FALSE, 43 | /** 44 | * Sets the field to an empty String ("") if the value from 45 | * the Excel cell cannot be parsed. Applies only to String fields. 46 | */ 47 | DEFAULT_TO_EMPTY_STRING, 48 | /** 49 | * Sets the field to the MIN_VALUE of the Field type. 50 | *
    51 | *
  • Long.MIN_VALUE for long, Long fields e.g. private long id;
  • 52 | *
  • Integer.MIN_VALUE for int, Integer fields e.g. private int id;
  • 53 | *
  • Double.MIN_VALUE for double, Double fields e.g. private double amount;
  • 54 | *
  • Float.MIN_VALUE for float, Float fields e.g. private float id;
  • 55 | *
56 | * 57 | */ 58 | DEFAULT_TO_MIN_VALUE, 59 | /** 60 | * Throws a {@link com.creditdatamw.zerocell.ZeroCellException} if the cell 61 | * value cannot be parsed and converted properly. 62 | *

NOTE:The library will NOT process the rest of the 63 | * row (and File, actually) when this occurs.

64 | */ 65 | THROW_EXCEPTION; 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/FloatConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | class FloatConverter extends DefaultConverter { 7 | private static final String message = "Failed to parse '%s' as Float at column='%s' row='%s'"; 8 | @Override 9 | public Float convert(String value, String columnName, int row) { 10 | try { 11 | return Float.parseFloat(value); 12 | } catch (Exception e) { 13 | switch (this.getFallbackStrategy()) { 14 | case LEGACY: 15 | case DEFAULT_TO_NULL: 16 | return null; 17 | case DEFAULT_TO_ZERO: 18 | return 0F; 19 | case THROW_EXCEPTION: 20 | throw new ZeroCellException(String.format(message, value, columnName, row)); 21 | case IGNORE: 22 | throw new IgnoreInvalidValueException(); 23 | case DEFAULT_TO_MIN_VALUE: 24 | case DEFAULT: 25 | default: 26 | return Float.MIN_VALUE; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/IntegerConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | public class IntegerConverter extends DefaultConverter { 7 | private static final String message = "Failed to parse '%s' as Integer at column='%s' row='%s'"; 8 | 9 | @Override 10 | public Integer convert(String value, String columnName, int row) { 11 | try { 12 | return Integer.parseInt(value); 13 | } catch (Exception e) { 14 | switch (this.getFallbackStrategy()) { 15 | case LEGACY: 16 | case DEFAULT_TO_NULL: 17 | return null; 18 | case DEFAULT_TO_ZERO: 19 | return 0; 20 | case THROW_EXCEPTION: 21 | throw new ZeroCellException(String.format(message, value, columnName, row)); 22 | case IGNORE: 23 | throw new IgnoreInvalidValueException(); 24 | case DEFAULT_TO_MIN_VALUE: 25 | case DEFAULT: 26 | default: 27 | return Integer.MIN_VALUE; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/LocalDateConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | import java.time.LocalDate; 7 | 8 | public class LocalDateConverter extends DefaultConverter { 9 | private static final String message = "Failed to parse '%s' as java.time.LocalDate at column='%s' row='%s'"; 10 | @Override 11 | public LocalDate convert(String value, String columnName, int row) { 12 | try { 13 | return LocalDate.parse(value); 14 | } catch(Exception e) { 15 | switch (this.getFallbackStrategy()) { 16 | case LEGACY: 17 | case DEFAULT_TO_NULL: 18 | return null; 19 | case IGNORE: 20 | throw new IgnoreInvalidValueException(); 21 | case DEFAULT: 22 | case THROW_EXCEPTION: 23 | default: 24 | throw new ZeroCellException(String.format(message, value, columnName, row)); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/LocalDateTimeConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | import java.time.LocalDateTime; 7 | 8 | public class LocalDateTimeConverter extends DefaultConverter { 9 | private static final String message = "Failed to parse '%s' as java.time.LocalDateTime at column='%s' row='%s'"; 10 | 11 | @Override 12 | public LocalDateTime convert(String value, String columnName, int row) { 13 | try { 14 | return LocalDateTime.parse(value); 15 | } catch(Exception e) { 16 | switch (this.getFallbackStrategy()) { 17 | case LEGACY: 18 | case DEFAULT_TO_NULL: 19 | return null; 20 | case IGNORE: 21 | throw new IgnoreInvalidValueException(); 22 | case DEFAULT: 23 | case THROW_EXCEPTION: 24 | default: 25 | throw new ZeroCellException(String.format(message, value, columnName, row)); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/LongConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | public class LongConverter extends DefaultConverter { 7 | private static final String message = "Failed to parse '%s' as Long at column='%s' row='%s'"; 8 | @Override 9 | public Long convert(String value, String columnName, int row) { 10 | try { 11 | return Long.parseLong(value); 12 | } catch (Exception e) { 13 | switch (this.getFallbackStrategy()) { 14 | case LEGACY: 15 | case DEFAULT_TO_NULL: 16 | return null; 17 | case DEFAULT_TO_ZERO: 18 | return 0L; 19 | case THROW_EXCEPTION: 20 | throw new ZeroCellException(String.format(message, value, columnName, row)); 21 | case IGNORE: 22 | throw new IgnoreInvalidValueException(); 23 | case DEFAULT_TO_MIN_VALUE: 24 | case DEFAULT: 25 | default: 26 | return Long.MIN_VALUE; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/NoopConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | /** 4 | * Empty Converter class - returns the same exact value provided. 5 | */ 6 | public final class NoopConverter extends DefaultConverter { 7 | @Override 8 | public String convert(String value, String column, int row) { 9 | return value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/SqlDateConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | import java.sql.Date; 7 | 8 | public class SqlDateConverter extends DefaultConverter { 9 | private static final String message = "Failed to parse '%s' as java.sql.Date at column='%s' row='%s'"; 10 | 11 | @Override 12 | public Date convert(String value, String columnName, int row) { 13 | try { 14 | return Date.valueOf(value); 15 | } catch (Exception e) { 16 | switch (this.getFallbackStrategy()) { 17 | case LEGACY: 18 | case DEFAULT_TO_NULL: 19 | return null; 20 | case IGNORE: 21 | throw new IgnoreInvalidValueException(); 22 | case DEFAULT: 23 | case THROW_EXCEPTION: 24 | default: 25 | throw new ZeroCellException(String.format(message, value, columnName, row)); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/converter/SqlTimestampConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | 6 | import java.sql.Timestamp; 7 | 8 | public class SqlTimestampConverter extends DefaultConverter { 9 | private static final String message = "Failed to parse '%s' as java.sql.Timestamp at column='%s' row='%s'"; 10 | @Override 11 | public Timestamp convert(String value, String columnName, int row) { 12 | try { 13 | return Timestamp.valueOf(value); 14 | } catch (Exception e) { 15 | switch (this.getFallbackStrategy()) { 16 | case LEGACY: 17 | case DEFAULT_TO_NULL: 18 | return null; 19 | case IGNORE: 20 | throw new IgnoreInvalidValueException(); 21 | case DEFAULT: 22 | case THROW_EXCEPTION: 23 | default: 24 | throw new ZeroCellException(String.format(message, value, columnName, row)); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/handler/EntityExcelSheetHandler.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.handler; 2 | 3 | import com.creditdatamw.zerocell.ReaderUtil; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | import com.creditdatamw.zerocell.ZeroCellReader; 6 | import com.creditdatamw.zerocell.column.ColumnInfo; 7 | import com.creditdatamw.zerocell.converter.Converter; 8 | import com.creditdatamw.zerocell.converter.NoopConverter; 9 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 10 | import org.apache.poi.ss.util.CellAddress; 11 | import org.apache.poi.ss.util.CellReference; 12 | import org.apache.poi.xssf.usermodel.XSSFComment; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.File; 17 | import java.lang.reflect.Field; 18 | import java.util.*; 19 | 20 | import static com.creditdatamw.zerocell.converter.ConverterUtils.convertValueToType; 21 | 22 | final class EntityExcelSheetHandler implements ZeroCellReader { 23 | private EntityHandler entityHandler; 24 | private final Logger LOGGER = LoggerFactory.getLogger(EntityExcelSheetHandler.class); 25 | private final ColumnFieldWriter columnFieldWriter = new ColumnFieldWriter(); 26 | 27 | private final ColumnInfo rowNumberColumn; 28 | private final Map columns; 29 | private final List entities; 30 | private final Converter NOOP_CONVERTER = new NoopConverter(); 31 | private final Map converters; 32 | private boolean isHeaderRow = false; 33 | private int currentRow = -1; 34 | private int currentCol = -1; 35 | private EmptyColumnCounter emptyColumnCounter = new EmptyColumnCounter(); 36 | private T cur; 37 | 38 | EntityExcelSheetHandler(EntityHandler entityHandler, ColumnInfo rowNumberColumn, Map columns) { 39 | this.entityHandler = entityHandler; 40 | this.rowNumberColumn = rowNumberColumn; 41 | this.columns = columns; 42 | this.converters = cacheConverters(); 43 | this.entities = new ArrayList<>(); 44 | } 45 | 46 | private Map cacheConverters() { 47 | Map mappedConverters = new HashMap<>(); 48 | for (ColumnInfo columnInfo : columns.values()) { 49 | mappedConverters.put(columnInfo.getIndex(), NOOP_CONVERTER); 50 | try { 51 | if (!columnInfo.getConverterClass().equals(NoopConverter.class)) { 52 | mappedConverters.put(columnInfo.getIndex(), (Converter) columnInfo.getConverterClass().newInstance()); 53 | } 54 | } catch (InstantiationException | IllegalAccessException e) { 55 | LOGGER.error("Failed to instantiate Converter class: {}", columnInfo.getConverterClass()); 56 | } 57 | } 58 | return mappedConverters; 59 | } 60 | 61 | /** 62 | * Returns a list of entities loaded from the provided Excel file 63 | * @param file the Excel file to load data from 64 | * @param sheet the sheet to load data from 65 | * @return list of entities from the sheet 66 | */ 67 | @Override 68 | public List read(File file, String sheet) { 69 | /** 70 | * We don't need to process the file here since that's 71 | * handled in {@link ReaderUtil} which MUST be used when using this class 72 | */ 73 | return Collections.unmodifiableList(this.entities); 74 | } 75 | 76 | void clear() { 77 | this.currentRow = -1; 78 | this.currentCol = -1; 79 | this.cur = null; 80 | this.entities.clear(); 81 | } 82 | 83 | @Override 84 | @SuppressWarnings("unchecked") 85 | public void startRow(int i) { 86 | currentRow = i; 87 | //skip the current row 88 | if (currentRow - entityHandler.getSkipFirstNRows() < 0) return; 89 | currentRow = currentRow - entityHandler.getSkipFirstNRows(); 90 | // skip the header row 91 | if (currentRow == 0) { 92 | isHeaderRow = true; 93 | return; 94 | } else { 95 | isHeaderRow = false; 96 | } 97 | try { 98 | cur = (T) entityHandler.getEntityClass().newInstance(); 99 | // Write to the field with the @RowNumber annotation here if it exists 100 | if (!Objects.isNull(rowNumberColumn)) { 101 | writeColumnField(cur, String.valueOf(currentRow), rowNumberColumn, currentRow); 102 | } 103 | } catch (InstantiationException | IllegalAccessException e) { 104 | throw new ZeroCellException("Failed to create and instance of " + entityHandler.getEntityClass().getName(), e); 105 | } 106 | } 107 | 108 | private boolean isRowNumberValueSetted() { 109 | return entityHandler.getMaxRowNumber() != 0 && 110 | entityHandler.getMaxRowNumber() != entityHandler.getSkipFirstNRows(); 111 | } 112 | 113 | @Override 114 | public void endRow(int i) { 115 | if (isRowNumberValueSetted() && i > entityHandler.getMaxRowNumber()) { 116 | return; 117 | } 118 | 119 | if (!Objects.isNull(cur)) { 120 | if (entityHandler.isSkipEmptyRows() && emptyColumnCounter.rowIsEmpty()) { 121 | LOGGER.warn("Row#{} skipped because it is empty", i); 122 | } else { 123 | this.entities.add(cur); 124 | } 125 | } 126 | cur = null; 127 | emptyColumnCounter.reset(); 128 | } 129 | 130 | @Override 131 | public void cell(String cellReference, String formattedValue, XSSFComment xssfComment) { 132 | if (cellReference == null) { 133 | cellReference = new CellAddress(currentRow, currentCol).formatAsString(); 134 | } 135 | int column = new CellReference(cellReference).getCol(); 136 | currentCol = column; 137 | 138 | ColumnInfo currentColumnInfo = columns.get(column); 139 | if (Objects.isNull(currentColumnInfo)) { 140 | return; 141 | } 142 | 143 | if (isHeaderRow && !entityHandler.isSkipHeaderRow()) { 144 | if (!currentColumnInfo.getName().equalsIgnoreCase(formattedValue.trim())) { 145 | throw new ZeroCellException(String.format("Expected Column '%s' but found '%s'", currentColumnInfo.getName(), formattedValue)); 146 | } 147 | } 148 | // Prevent from trying to write to a null instance 149 | if (Objects.isNull(cur)) return; 150 | if (entityHandler.isSkipEmptyRows()) { 151 | if (formattedValue == null || formattedValue.isEmpty()) { 152 | emptyColumnCounter.increment(); 153 | } 154 | } 155 | writeColumnField(cur, formattedValue, currentColumnInfo, currentRow); 156 | } 157 | 158 | 159 | /** 160 | * Write the value read from the excel cell to a field 161 | * 162 | * @param object the object to write to 163 | * @param formattedValue the value read from the current excel column/row 164 | * @param currentColumnInfo Column metadata 165 | * @param rowNum the row number 166 | */ 167 | private void writeColumnField(T object, String formattedValue, ColumnInfo currentColumnInfo, int rowNum) { 168 | Converter converter = NOOP_CONVERTER; 169 | if (currentColumnInfo.getIndex() != -1) { 170 | converter = converters.get(currentColumnInfo.getIndex()); 171 | } 172 | try { 173 | Field field = entityHandler.getEntityClass() 174 | .getDeclaredField(currentColumnInfo.getFieldName()); 175 | 176 | columnFieldWriter.writeColumnField( 177 | field, 178 | cur, 179 | formattedValue, 180 | currentColumnInfo, 181 | rowNum, 182 | converter 183 | ); 184 | } catch (NoSuchFieldException e) { 185 | LOGGER.error("Failed to set field '{}' because it does not exist. Got 'NoSuchFieldException'", currentColumnInfo.getFieldName()); 186 | return; 187 | } 188 | } 189 | 190 | @Override 191 | public void headerFooter(String text, boolean b, String tagName) { 192 | // Skip, no headers or footers in CSV 193 | } 194 | 195 | /** 196 | * Handles writing of values to a field on an object. 197 | */ 198 | static class ColumnFieldWriter { 199 | /** 200 | * Write the value read from the excel cell to a field 201 | * 202 | * @param object the object to write to 203 | * @param formattedValue the value read from the current excel column/row 204 | * @param currentColumnInfo Column metadata 205 | * @param rowNum the row number 206 | */ 207 | @SuppressWarnings("unchecked") 208 | void writeColumnField(Field field, 209 | T object, 210 | String formattedValue, 211 | ColumnInfo currentColumnInfo, 212 | int rowNum, 213 | Converter converter) { 214 | 215 | String fieldName = currentColumnInfo.getFieldName(); 216 | // For values that come with comma's sanitize the input 217 | Class fieldType = currentColumnInfo.getType(); 218 | if (fieldType == Integer.class || fieldType == int.class || 219 | fieldType == Long.class || fieldType == long.class || fieldType == Double.class || 220 | fieldType == double.class || fieldType == Float.class || fieldType == float.class) { 221 | formattedValue = formattedValue.replace(",", ""); 222 | } 223 | 224 | try { 225 | Object value = null; 226 | // Don't use a converter if there isn't a custom one 227 | if (converter instanceof NoopConverter) { 228 | try { 229 | value = convertValueToType( 230 | currentColumnInfo.getType(), 231 | formattedValue, 232 | currentColumnInfo.getName(), 233 | rowNum, 234 | currentColumnInfo.getFallbackStrategy() 235 | ); 236 | } catch(IgnoreInvalidValueException d) { 237 | // When this exception is thrown it means don't set any 238 | // value of there was a failure to parse the formattedValue 239 | // into the appropriate type 240 | return; 241 | } 242 | } else { 243 | // Handle any exceptions thrown by the converter - this stops execution of the whole process 244 | try { 245 | value = converter.convert(formattedValue, currentColumnInfo.getName(), rowNum); 246 | } catch (Exception e) { 247 | String messageTemplate = "Failed to convert value '%s' at Column(name='%s', index='%s', row='%s')" ; 248 | String message = String.format(messageTemplate, 249 | formattedValue, 250 | currentColumnInfo.getName(), 251 | currentColumnInfo.getIndex(), 252 | rowNum); 253 | 254 | LoggerFactory.getLogger(EntityExcelSheetHandler.class) 255 | .warn(message + " using: " + converter.getClass().getName(), e); 256 | throw new ZeroCellException(message + " using: " + converter.getClass().getSimpleName(), e); 257 | } 258 | } 259 | boolean access = field.isAccessible(); 260 | if (!access) { 261 | field.setAccessible(true); 262 | } 263 | field.set(object, value); 264 | field.setAccessible(field.isAccessible() && access); 265 | } catch (IllegalArgumentException | IllegalAccessException e) { 266 | throw new ZeroCellException(String.format("Failed to write value %s to field %s at row %s", formattedValue, fieldName, rowNum)); 267 | } 268 | } 269 | } 270 | 271 | private class EmptyColumnCounter { 272 | private int count = 0; 273 | 274 | void increment() { 275 | this.count += 1; 276 | } 277 | 278 | boolean rowIsEmpty() { 279 | return this.count >= columns.size(); 280 | } 281 | void reset() { 282 | count = 0; 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/handler/EntityHandler.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.handler; 2 | 3 | import com.creditdatamw.zerocell.ReaderUtil; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | import com.creditdatamw.zerocell.column.ColumnInfo; 6 | import com.creditdatamw.zerocell.column.ColumnMapping; 7 | import com.creditdatamw.zerocell.column.RowNumberInfo; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.File; 12 | import java.io.InputStream; 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | 18 | import static com.creditdatamw.zerocell.column.ColumnMapping.parseColumnMappingFromAnnotations; 19 | 20 | public class EntityHandler { 21 | private static final Logger LOGGER = LoggerFactory.getLogger(EntityHandler.class); 22 | 23 | public static final String DEFAULT_SHEET = "ZEROCELL_READ_FIRST_SHEET_0"; 24 | 25 | private final Class type; 26 | private final EntityExcelSheetHandler entitySheetHandler; 27 | private final String sheetName; 28 | private final boolean skipHeaderRow; 29 | private boolean skipEmptyRows; 30 | private final int skipFirstNRows; 31 | private final int maxRowNumber; 32 | 33 | public EntityHandler(Class clazz, boolean skipHeaderRow, int skipFirstNRows, int maxRowNumber) { 34 | Objects.requireNonNull(clazz); 35 | assert (skipFirstNRows >= 0); 36 | this.type = clazz; 37 | this.sheetName = DEFAULT_SHEET; 38 | this.entitySheetHandler = createSheetHandler(clazz, null); 39 | this.skipHeaderRow = skipHeaderRow; 40 | this.skipFirstNRows = skipFirstNRows; 41 | this.maxRowNumber = maxRowNumber; 42 | } 43 | 44 | public EntityHandler(Class clazz, String sheetName, boolean skipHeaderRow, int skipFirstNRows, int maxRowNumber) { 45 | Objects.requireNonNull(clazz); 46 | assert (skipFirstNRows >= 0); 47 | this.type = clazz; 48 | this.sheetName = sheetName; 49 | this.entitySheetHandler = createSheetHandler(clazz, null); 50 | this.skipHeaderRow = skipHeaderRow; 51 | this.skipFirstNRows = skipFirstNRows; 52 | this.maxRowNumber = maxRowNumber; 53 | } 54 | 55 | public EntityHandler(Class clazz, ColumnMapping columnMapping, boolean skipHeaderRow, int skipFirstNRows, int maxRowNumber) { 56 | Objects.requireNonNull(clazz); 57 | assert (skipFirstNRows >= 0); 58 | this.type = clazz; 59 | this.sheetName = DEFAULT_SHEET; 60 | this.entitySheetHandler = createSheetHandler(clazz, columnMapping); 61 | this.skipHeaderRow = skipHeaderRow; 62 | this.skipFirstNRows = skipFirstNRows; 63 | this.maxRowNumber = maxRowNumber; 64 | } 65 | 66 | public EntityHandler(Class clazz, String sheetName, ColumnMapping columnMapping, boolean skipHeaderRow, int skipFirstNRows, int maxRowNumber) { 67 | Objects.requireNonNull(clazz); 68 | assert (skipFirstNRows >= 0); 69 | this.type = clazz; 70 | this.sheetName = sheetName; 71 | this.entitySheetHandler = createSheetHandler(clazz, columnMapping); 72 | this.skipHeaderRow = skipHeaderRow; 73 | this.skipFirstNRows = skipFirstNRows; 74 | this.maxRowNumber = maxRowNumber; 75 | } 76 | 77 | private boolean isRowNumberValueSetted() { 78 | return this.maxRowNumber != 0 && this.maxRowNumber != this.skipFirstNRows; 79 | } 80 | 81 | public String getSheetName() { 82 | return sheetName; 83 | } 84 | 85 | public EntityExcelSheetHandler getEntitySheetHandler() { 86 | return entitySheetHandler; 87 | } 88 | 89 | public boolean isSkipHeaderRow() { 90 | return skipHeaderRow; 91 | } 92 | 93 | public int getSkipFirstNRows() { 94 | return skipFirstNRows; 95 | } 96 | 97 | public int getMaxRowNumber() { 98 | return maxRowNumber; 99 | } 100 | 101 | public boolean isSkipEmptyRows() { 102 | return skipEmptyRows; 103 | } 104 | 105 | public void setSkipEmptyRows(boolean skipEmptyRows) { 106 | this.skipEmptyRows = skipEmptyRows; 107 | } 108 | 109 | @SuppressWarnings("unchecked") 110 | private EntityExcelSheetHandler createSheetHandler(Class clazz, ColumnMapping columnMapping) { 111 | if (columnMapping == null) { 112 | columnMapping = parseColumnMappingFromAnnotations(clazz); 113 | } 114 | final RowNumberInfo rowNumberColumn = columnMapping.getRowNumberInfo(); 115 | Map infoMap = columnMapping.getColumnsMap(); 116 | return new EntityExcelSheetHandler(this, rowNumberColumn, infoMap); 117 | } 118 | 119 | /** 120 | * Returns the extracted entities as an immutable list. 121 | * 122 | * @return an immutable list of the extracted entities 123 | */ 124 | public List readAsList() { 125 | List list = Collections.unmodifiableList(this.entitySheetHandler.read(null, sheetName)); 126 | return list; 127 | } 128 | 129 | public void process(File file) throws ZeroCellException { 130 | ReaderUtil.process(file, sheetName, this.entitySheetHandler); 131 | } 132 | 133 | public void process(InputStream inputStream) { 134 | ReaderUtil.process(inputStream, sheetName, this.entitySheetHandler); 135 | } 136 | 137 | public Class getEntityClass() { 138 | return type; 139 | } 140 | } -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/internal/IgnoreInvalidValueException.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.internal; 2 | 3 | import com.creditdatamw.zerocell.converter.FallbackStrategy; 4 | 5 | /** 6 | * This is a special exception that accompanies the {@link FallbackStrategy#IGNORE} 7 | * strategy. When this exception is thrown, the EntitySheetHandler MUST not 8 | * attempt to set the value on the field. 9 | */ 10 | public class IgnoreInvalidValueException extends RuntimeException { 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/creditdatamw/zerocell/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal utility classes for zerocell. Not meant to be used in user code. 3 | * Here be dragons. 4 | */ 5 | package com.creditdatamw.zerocell.internal; -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/Person.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.annotation.Column; 4 | import com.creditdatamw.zerocell.annotation.RowNumber; 5 | 6 | import java.sql.Date; 7 | import java.time.LocalDate; 8 | 9 | /** 10 | * PersonExample Class for tests 11 | */ 12 | public class Person { 13 | @RowNumber 14 | private int rowNumber; 15 | 16 | @Column(name= "ID", index=0) 17 | private String id; 18 | 19 | @Column(name = "FIRST_NAME", index = 1) 20 | private String firstName; 21 | 22 | @Column(name = "MIDDLE_NAME", index = 2) 23 | private String middleName; 24 | 25 | @Column(name = "LAST_NAME", index = 3) 26 | private String lastName; 27 | 28 | @Column(name = "DATE_OF_BIRTH", index = 4) 29 | private LocalDate dateOfBirth; 30 | 31 | @Column(name = "DATE_REGISTERED", index = 6) 32 | private Date dateOfRegistration; 33 | 34 | @Column(name = "FAV_NUMBER", index = 5) 35 | private int favouriteNumber; 36 | 37 | public Person() { } 38 | 39 | public int getRowNumber() { 40 | return rowNumber; 41 | } 42 | 43 | public void setRowNumber(int rowNumber) { 44 | this.rowNumber = rowNumber; 45 | } 46 | 47 | public String getId() { 48 | return id; 49 | } 50 | 51 | public void setId(String id) { 52 | this.id = id; 53 | } 54 | 55 | public String getFirstName() { 56 | return firstName; 57 | } 58 | 59 | public void setFirstName(String firstName) { 60 | this.firstName = firstName; 61 | } 62 | 63 | public String getMiddleName() { 64 | return middleName; 65 | } 66 | 67 | public void setMiddleName(String middleName) { 68 | this.middleName = middleName; 69 | } 70 | 71 | public String getLastName() { 72 | return lastName; 73 | } 74 | 75 | public void setLastName(String lastName) { 76 | this.lastName = lastName; 77 | } 78 | 79 | public Date getDateOfRegistration() { 80 | return dateOfRegistration; 81 | } 82 | 83 | public void setDateOfRegistration(Date dateOfRegistration) { 84 | this.dateOfRegistration = dateOfRegistration; 85 | } 86 | 87 | public LocalDate getDateOfBirth() { 88 | return dateOfBirth; 89 | } 90 | 91 | public void setDateOfBirth(LocalDate dateOfBirth) { 92 | this.dateOfBirth = dateOfBirth; 93 | } 94 | 95 | public int getFavouriteNumber() { 96 | return favouriteNumber; 97 | } 98 | 99 | public void setFavouriteNumber(int favouriteNumber) { 100 | this.favouriteNumber = favouriteNumber; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/TestHandler.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.annotation.Column; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | 8 | import java.io.File; 9 | 10 | public class TestHandler { 11 | 12 | @Rule 13 | public ExpectedException thrown = ExpectedException.none(); 14 | 15 | @Test 16 | public void testFS701MissingColumnIssue() { 17 | // A row class with an index skipped must not be allowed 18 | thrown.expect(ZeroCellException.class); 19 | thrown.expectMessage(String.format("Expected Column '%s' but found '%s'", "FIRST", "ID")); 20 | Reader.of(Row.class) 21 | .from(new File("src/test/resources/test_people.xlsx")) 22 | .sheet("uploads") 23 | .list(); 24 | } 25 | 26 | public static class Row { 27 | @Column(index = 0, name ="First") String first; 28 | @Column(index = 1, name ="Second") String second; 29 | @Column(index = 3, name ="Third") String third; 30 | 31 | public Row() { 32 | } 33 | 34 | public String getFirst() { 35 | return first; 36 | } 37 | 38 | public void setFirst(String first) { 39 | this.first = first; 40 | } 41 | 42 | public String getSecond() { 43 | return second; 44 | } 45 | 46 | public void setSecond(String second) { 47 | this.second = second; 48 | } 49 | 50 | public String getThird() { 51 | return third; 52 | } 53 | 54 | public void setThird(String third) { 55 | this.third = third; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/TestProgrammaticApi.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.column.ColumnInfo; 4 | import com.creditdatamw.zerocell.column.RowNumberInfo; 5 | import org.junit.Test; 6 | 7 | import java.io.File; 8 | import java.sql.Date; 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class TestProgrammaticApi { 15 | 16 | @Test 17 | public void testCanCreateReaderWithConfiguration() { 18 | List people = Reader.of(PersonWithoutAnnotation.class) 19 | .from(new File("src/test/resources/test_people.xlsx")) 20 | .using( 21 | new RowNumberInfo("rowNumber", Integer.class), 22 | new ColumnInfo("ID", "id", 0, String.class), 23 | new ColumnInfo("FIRST_NAME", "firstName", 1, String.class), 24 | new ColumnInfo("MIDDLE_NAME", "middleName", 2, String.class), 25 | new ColumnInfo("LAST_NAME", "lastName", 3, String.class), 26 | new ColumnInfo("DATE_OF_BIRTH", "dateOfBirth", 4, LocalDate.class), 27 | new ColumnInfo("DATE_REGISTERED", "dateOfRegistration", 6, Date.class), 28 | new ColumnInfo("FAV_NUMBER", "favouriteNumber", 5, Integer.class) 29 | ) 30 | .sheet("uploads") 31 | .list(); 32 | assertNotNull(people); 33 | assertFalse(people.isEmpty()); 34 | assertEquals(5, people.size()); 35 | 36 | PersonWithoutAnnotation zikani = people.get(0); 37 | 38 | assertEquals(1, zikani.getRowNumber()); 39 | assertEquals("Zikani", zikani.getFirstName()); 40 | } 41 | 42 | public static class PersonWithoutAnnotation { 43 | private int rowNumber; 44 | private String id; 45 | private String firstName; 46 | private String middleName; 47 | private String lastName; 48 | private LocalDate dateOfBirth; 49 | private Date dateOfRegistration; 50 | private int favouriteNumber; 51 | 52 | public PersonWithoutAnnotation() { } 53 | 54 | public int getRowNumber() { 55 | return rowNumber; 56 | } 57 | 58 | public void setRowNumber(int rowNumber) { 59 | this.rowNumber = rowNumber; 60 | } 61 | 62 | public String getId() { 63 | return id; 64 | } 65 | 66 | public void setId(String id) { 67 | this.id = id; 68 | } 69 | 70 | public String getFirstName() { 71 | return firstName; 72 | } 73 | 74 | public void setFirstName(String firstName) { 75 | this.firstName = firstName; 76 | } 77 | 78 | public String getMiddleName() { 79 | return middleName; 80 | } 81 | 82 | public void setMiddleName(String middleName) { 83 | this.middleName = middleName; 84 | } 85 | 86 | public String getLastName() { 87 | return lastName; 88 | } 89 | 90 | public void setLastName(String lastName) { 91 | this.lastName = lastName; 92 | } 93 | 94 | public Date getDateOfRegistration() { 95 | return dateOfRegistration; 96 | } 97 | 98 | public void setDateOfRegistration(Date dateOfRegistration) { 99 | this.dateOfRegistration = dateOfRegistration; 100 | } 101 | 102 | public LocalDate getDateOfBirth() { 103 | return dateOfBirth; 104 | } 105 | 106 | public void setDateOfBirth(LocalDate dateOfBirth) { 107 | this.dateOfBirth = dateOfBirth; 108 | } 109 | 110 | public int getFavouriteNumber() { 111 | return favouriteNumber; 112 | } 113 | 114 | public void setFavouriteNumber(int favouriteNumber) { 115 | this.favouriteNumber = favouriteNumber; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/TestReader.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | 4 | import com.creditdatamw.zerocell.annotation.Column; 5 | import com.creditdatamw.zerocell.annotation.RowNumber; 6 | import com.creditdatamw.zerocell.column.ColumnInfo; 7 | import com.creditdatamw.zerocell.column.RowNumberInfo; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | import org.junit.rules.TemporaryFolder; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.sql.Date; 19 | import java.time.LocalDate; 20 | import java.util.List; 21 | 22 | import static org.junit.Assert.*; 23 | 24 | /** 25 | * Unit Tests for the reader class 26 | */ 27 | public class TestReader { 28 | 29 | @Rule 30 | public TemporaryFolder temporaryFolder = new TemporaryFolder(); 31 | 32 | @Rule 33 | public ExpectedException thrown = ExpectedException.none(); 34 | 35 | @Test 36 | public void testShouldExtractPeopleFromFile() { 37 | List people = Reader.of(Person.class) 38 | .from(new File("src/test/resources/test_people.xlsx")) 39 | .sheet("uploads") 40 | .list(); 41 | assertNotNull(people); 42 | assertFalse(people.isEmpty()); 43 | assertEquals(5, people.size()); 44 | 45 | Person zikani = people.get(0); 46 | 47 | assertEquals(1, zikani.getRowNumber()); 48 | assertEquals("Zikani", zikani.getFirstName()); 49 | } 50 | 51 | @Test 52 | public void testShouldSkipFirst3RowsFromFile() { 53 | List people = Reader.of(Person.class) 54 | .from(new File("src/test/resources/test_people_with_offset_header.xlsx")) 55 | .sheet("uploads") 56 | .skipFirstNRows(3) 57 | .list(); 58 | assertNotNull(people); 59 | assertFalse(people.isEmpty()); 60 | assertEquals(5, people.size()); 61 | 62 | Person zikani = people.get(0); 63 | 64 | assertEquals(1, zikani.getRowNumber()); 65 | assertEquals("Zikani", zikani.getFirstName()); 66 | } 67 | 68 | @Test 69 | public void testShouldReadFromInputStream() throws IOException { 70 | InputStream inputStream = Files.newInputStream( 71 | Paths.get("src", "test", "resources", "test_people.xlsx")); 72 | 73 | List persons = Reader.of(Person.class) 74 | .from(inputStream) 75 | .sheet("uploads") 76 | .using( 77 | new RowNumberInfo("rowNumber", Integer.class), 78 | new ColumnInfo("ID", "id", 0, String.class), 79 | new ColumnInfo("FIRST_NAME", "firstName", 1, String.class), 80 | new ColumnInfo("MIDDLE_NAME", "middleName", 2, String.class), 81 | new ColumnInfo("LAST_NAME", "lastName", 3, String.class), 82 | new ColumnInfo("DATE_OF_BIRTH", "dateOfBirth", 4, LocalDate.class), 83 | new ColumnInfo("FAV_NUMBER", "favouriteNumber", 5, Integer.class), 84 | new ColumnInfo("DATE_REGISTERED", "dateOfRegistration", 6, Date.class) 85 | ) 86 | .list(); 87 | 88 | assertNotNull(persons); 89 | assertSame(5, persons.size()); 90 | assertEquals("Mwafulirwa", persons.get(2).getLastName()); 91 | assertEquals(1, persons.get(4).getFavouriteNumber()); 92 | } 93 | 94 | @Test 95 | public void testShouldExtractColumns() { 96 | String[] columnNames = new String[] { 97 | "ID", "FIRST_NAME", "MIDDLE_NAME", "LAST_NAME", "DATE_OF_BIRTH", "FAV_NUMBER", "DATE_REGISTERED" 98 | }; 99 | 100 | assertArrayEquals(columnNames, Reader.columnsOf(Person.class)); 101 | } 102 | 103 | @Test 104 | public void testShouldThrowOnInvalidSheetName() { 105 | thrown.expect(ZeroCellException.class); 106 | thrown.expectMessage("Could not find sheet INVALID_SHEET_NAME"); 107 | Reader.of(Person.class) 108 | .from(new File("src/test/resources/test_people.xlsx")) 109 | .sheet("INVALID_SHEET_NAME") 110 | .list(); 111 | } 112 | 113 | @Test 114 | public void testShouldThrowOnDuplicateIndex() { 115 | thrown.expect(ZeroCellException.class); 116 | thrown.expectMessage("Cannot map two columns to the same index: 0"); 117 | Reader.of(DuplicateIndex.class) 118 | .from(new File("src/test/resources/test_people.xlsx")) 119 | .sheet("uploads") 120 | .list(); 121 | } 122 | 123 | @Test 124 | public void testShouldThrowOnIncorrectColumnName() { 125 | thrown.expect(ZeroCellException.class); 126 | thrown.expectMessage("Expected Column 'DOB' but found 'DATE_OF_BIRTH'"); 127 | Reader.of(Person2.class) 128 | .from(new File("src/test/resources/test_people.xlsx")) 129 | .sheet("uploads") 130 | .list(); 131 | } 132 | 133 | @Test 134 | public void testShouldThrowForNonOpenXMLFile() throws IOException { 135 | File file = temporaryFolder.newFile(); 136 | thrown.expect(ZeroCellException.class); 137 | thrown.expectMessage("Cannot load file. The file must be an Excel 2007+ Workbook (.xlsx)"); 138 | Reader.of(Person.class) 139 | .from(file) 140 | .sheet("uploads") 141 | .list(); 142 | } 143 | 144 | public static class DuplicateIndex { 145 | @Column(name = "ID", index = 0) 146 | private String id1; 147 | 148 | @Column(name = "ID 2", index = 0) 149 | private String id2; 150 | 151 | public DuplicateIndex() { 152 | } 153 | 154 | public String getId1() { 155 | return id1; 156 | } 157 | 158 | public void setId1(String id1) { 159 | this.id1 = id1; 160 | } 161 | 162 | public String getId2() { 163 | return id2; 164 | } 165 | 166 | public void setId2(String id2) { 167 | this.id2 = id2; 168 | } 169 | } 170 | 171 | public static class Person2 { 172 | @RowNumber 173 | private int rowNumber; 174 | 175 | @Column(name= "ID", index=0) 176 | private String id; 177 | 178 | @Column(name = "FIRST_NAME", index = 1) 179 | private String firstName; 180 | 181 | @Column(name = "MIDDLE_NAME", index = 2) 182 | private String middleName; 183 | 184 | @Column(name = "LAST_NAME", index = 3) 185 | private String lastName; 186 | 187 | @Column(name = "DOB", index = 4) 188 | private LocalDate dateOfBirth; 189 | 190 | @Column(name = "DATE_REGISTERED", index = 6) 191 | private Date dateOfRegistration; 192 | 193 | @Column(name = "FAV_NUMBER", index = 5) 194 | private int favouriteNumber; 195 | 196 | public Person2() { 197 | } 198 | 199 | public int getRowNumber() { 200 | return rowNumber; 201 | } 202 | 203 | public void setRowNumber(int rowNumber) { 204 | this.rowNumber = rowNumber; 205 | } 206 | 207 | public String getId() { 208 | return id; 209 | } 210 | 211 | public void setId(String id) { 212 | this.id = id; 213 | } 214 | 215 | public String getFirstName() { 216 | return firstName; 217 | } 218 | 219 | public void setFirstName(String firstName) { 220 | this.firstName = firstName; 221 | } 222 | 223 | public String getMiddleName() { 224 | return middleName; 225 | } 226 | 227 | public void setMiddleName(String middleName) { 228 | this.middleName = middleName; 229 | } 230 | 231 | public String getLastName() { 232 | return lastName; 233 | } 234 | 235 | public void setLastName(String lastName) { 236 | this.lastName = lastName; 237 | } 238 | 239 | public LocalDate getDateOfBirth() { 240 | return dateOfBirth; 241 | } 242 | 243 | public void setDateOfBirth(LocalDate dateOfBirth) { 244 | this.dateOfBirth = dateOfBirth; 245 | } 246 | 247 | public Date getDateOfRegistration() { 248 | return dateOfRegistration; 249 | } 250 | 251 | public void setDateOfRegistration(Date dateOfRegistration) { 252 | this.dateOfRegistration = dateOfRegistration; 253 | } 254 | 255 | public int getFavouriteNumber() { 256 | return favouriteNumber; 257 | } 258 | 259 | public void setFavouriteNumber(int favouriteNumber) { 260 | this.favouriteNumber = favouriteNumber; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/TestReaderUtil.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.handler.EntityHandler; 4 | import org.junit.Test; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.List; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class TestReaderUtil { 15 | 16 | @Test 17 | public void testCanProcessFilePath() { 18 | final EntityHandler entityHandler = new EntityHandler( 19 | Person.class, 20 | false, 21 | 0, 22 | 0 23 | ); 24 | 25 | ReaderUtil.process( 26 | "src/test/resources/test_people.xlsx", 27 | entityHandler.getSheetName(), 28 | entityHandler.getEntitySheetHandler() 29 | ); 30 | 31 | List people = entityHandler.readAsList(); 32 | 33 | assertNotNull(people); 34 | assertFalse(people.isEmpty()); 35 | assertEquals(5, people.size()); 36 | 37 | Person zikani = people.get(0); 38 | 39 | assertEquals(1, zikani.getRowNumber()); 40 | assertEquals("Zikani", zikani.getFirstName()); 41 | } 42 | 43 | @Test 44 | public void testCanProcessFile() { 45 | final EntityHandler entityHandler = new EntityHandler( 46 | Person.class, 47 | false, 48 | 0, 49 | 0 50 | ); 51 | ReaderUtil.process( 52 | new File("src/test/resources/test_people.xlsx"), 53 | entityHandler.getSheetName(), 54 | entityHandler.getEntitySheetHandler() 55 | ); 56 | 57 | List people = entityHandler.readAsList(); 58 | 59 | assertNotNull(people); 60 | assertFalse(people.isEmpty()); 61 | assertEquals(5, people.size()); 62 | 63 | Person zikani = people.get(0); 64 | 65 | assertEquals(1, zikani.getRowNumber()); 66 | assertEquals("Zikani", zikani.getFirstName()); 67 | } 68 | 69 | /** 70 | * Try to load from the first sheet, but the sheet we want to load from is 71 | * the "uploads" sheet which is the second sheet of the file. 72 | */ 73 | @Test 74 | public void testFailToProcessFirstSheet() { 75 | final EntityHandler entityHandler = new EntityHandler( 76 | Person.class, 77 | false, 78 | 0, 79 | 0 80 | ); 81 | try { 82 | ReaderUtil.process( 83 | new File("src/test/resources/test_people_with_offset_sheet.xlsx"), 84 | EntityHandler.DEFAULT_SHEET, 85 | entityHandler.getEntitySheetHandler() 86 | ); 87 | fail("Somehow read the correct sheet."); 88 | } catch(ZeroCellException e) { 89 | assertEquals("Expected Column 'ID' but found 'This sheet is the first but not the intended sheet'", e.getMessage()); 90 | } 91 | } 92 | 93 | @Test 94 | public void testCanProcessInputStream() throws IOException { 95 | final EntityHandler entityHandler = new EntityHandler( 96 | Person.class, 97 | false, 98 | 0, 99 | 0 100 | ); 101 | ReaderUtil.process( 102 | Files.newInputStream( 103 | Paths.get("src", "test", "resources", "test_people.xlsx")), 104 | entityHandler.getSheetName(), 105 | entityHandler.getEntitySheetHandler() 106 | ); 107 | 108 | List people = entityHandler.readAsList(); 109 | 110 | assertNotNull(people); 111 | assertFalse(people.isEmpty()); 112 | assertEquals(5, people.size()); 113 | 114 | Person zikani = people.get(0); 115 | 116 | assertEquals(1, zikani.getRowNumber()); 117 | assertEquals("Zikani", zikani.getFirstName()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/TestSkipEmptyRows.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell; 2 | 3 | import com.creditdatamw.zerocell.column.ColumnInfo; 4 | import com.creditdatamw.zerocell.column.RowNumberInfo; 5 | import org.junit.Test; 6 | 7 | import java.io.File; 8 | import java.sql.Date; 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class TestSkipEmptyRows { 15 | 16 | @Test 17 | public void testCanSkipEmptyRowsWithProgrammaticAPI() { 18 | List people = Reader.of(Person.class) 19 | .from(new File("src/test/resources/test_people_with_empty_rows.xlsx")) 20 | .using( 21 | new RowNumberInfo("rowNumber", Integer.class), 22 | new ColumnInfo("ID", "id", 0, String.class), 23 | new ColumnInfo("FIRST_NAME", "firstName", 1, String.class), 24 | new ColumnInfo("MIDDLE_NAME", "middleName", 2, String.class), 25 | new ColumnInfo("LAST_NAME", "lastName", 3, String.class), 26 | new ColumnInfo("DATE_OF_BIRTH", "dateOfBirth", 4, LocalDate.class), 27 | new ColumnInfo("DATE_REGISTERED", "dateOfRegistration", 6, Date.class), 28 | new ColumnInfo("FAV_NUMBER", "favouriteNumber", 5, Integer.class) 29 | ) 30 | .sheet("uploads") 31 | .skipEmptyRows(true) 32 | .list(); 33 | assertNotNull(people); 34 | assertFalse(people.isEmpty()); 35 | assertEquals(5, people.size()); 36 | 37 | Person zikani = people.get(0); 38 | 39 | assertEquals(1, zikani.getRowNumber()); 40 | assertEquals("Zikani", zikani.getFirstName()); 41 | } 42 | 43 | @Test 44 | public void testCanSkipEmptyRows() { 45 | List people = Reader.of(Person.class) 46 | .from(new File("src/test/resources/test_people_with_empty_rows.xlsx")) 47 | .sheet("uploads") 48 | .skipEmptyRows(true) 49 | .list(); 50 | assertNotNull(people); 51 | assertFalse(people.isEmpty()); 52 | assertEquals(5, people.size()); 53 | 54 | Person zikani = people.get(0); 55 | 56 | assertEquals(1, zikani.getRowNumber()); 57 | assertEquals("Zikani", zikani.getFirstName()); 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/column/TestColumnInfo.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class TestColumnInfo { 8 | @Test 9 | public void testConstructor() { 10 | ColumnInfo columnInfo = new ColumnInfo("COLUMN 1", "columnOne", 1, Integer.class); 11 | 12 | assertEquals("columnOne", columnInfo.getFieldName()); 13 | assertEquals(Integer.class, columnInfo.getType()); 14 | assertEquals("COLUMN 1", columnInfo.getName()); 15 | assertEquals(1, columnInfo.getIndex()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/column/TestColumnMapping.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | public class TestColumnMapping { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/column/TestRowNumberInfo.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.column; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class TestRowNumberInfo { 8 | @Test 9 | public void testConstructor() { 10 | RowNumberInfo rowNumberInfo = new RowNumberInfo("rowNo", Integer.class); 11 | 12 | assertEquals("rowNo", rowNumberInfo.getFieldName()); 13 | assertEquals(Integer.class, rowNumberInfo.getType()); 14 | assertEquals("__ROWNUMBER__", rowNumberInfo.getName()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/converter/TestBooleanConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class TestBooleanConverter { 8 | private BooleanConverter converter = new BooleanConverter(); 9 | @Test 10 | public void testBooleanConverter() { 11 | assertEquals(false, converter.withFallbackStrategy(FallbackStrategy.DEFAULT) 12 | .convert("false", "TEST_COLUMN", 1)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/converter/TestConverterUtils.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.creditdatamw.zerocell.converter.ConverterUtils.convertValueToType; 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertNotEquals; 8 | 9 | public class TestConverterUtils { 10 | 11 | @Test 12 | public void testConvertValuesToString() { 13 | assertEquals("Hello", convertValueToType(String.class, "Hello", "A", 1, FallbackStrategy.DEFAULT)); 14 | assertEquals("HELLO", convertValueToType(String.class, "HELLO", "A", 1, FallbackStrategy.DEFAULT)); 15 | assertEquals("1.0", convertValueToType(String.class, "1.0", "A", 1, FallbackStrategy.DEFAULT)); 16 | assertEquals("2019-01-01", convertValueToType(String.class, "2019-01-01", "A", 1, FallbackStrategy.DEFAULT)); 17 | } 18 | 19 | @Test 20 | public void testConvertValuesToLong() { 21 | assertNotEquals(Long.MIN_VALUE, convertValueToType(Long.class, "Hello", "A", 1, FallbackStrategy.DEFAULT_TO_NULL)); 22 | assertEquals(null, convertValueToType(Long.class, "Hello", "A", 1, FallbackStrategy.DEFAULT_TO_NULL)); 23 | 24 | long v = (long) convertValueToType(Long.class, "Hello", "A", 1, FallbackStrategy.DEFAULT); 25 | assertEquals(Long.MIN_VALUE, v); 26 | 27 | assertEquals(Long.MIN_VALUE, convertValueToType(Long.class, "2 + 2", "A", 1, FallbackStrategy.DEFAULT)); 28 | assertEquals(-1L, convertValueToType(Long.class, "-1", "A", 1, FallbackStrategy.DEFAULT)); 29 | assertEquals(1L, convertValueToType(Long.class, "1", "A", 1, FallbackStrategy.DEFAULT)); 30 | } 31 | 32 | @Test 33 | public void testConvertValuesToLong() { 34 | assertNotEquals(Long.MIN_VALUE, convertValueToType(Long.class, "Hello", "A", 1)); 35 | assertEquals(null, convertValueToType(Long.class, "Hello", "A", 1)); 36 | 37 | long v = (long) convertValueToType(Long.class, "Hello", "A", 1); 38 | assertEquals(Long.MIN_VALUE, v); 39 | 40 | assertEquals(Long.MIN_VALUE, convertValueToType(Long.class, "2 + 2", "A", 1)); 41 | assertEquals(-1, convertValueToType(Long.class, "-1", "A", 1)); 42 | assertEquals(1, convertValueToType(Long.class, "1.0", "A", 1)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/converter/TestDoubleConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import com.creditdatamw.zerocell.internal.IgnoreInvalidValueException; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertNotEquals; 8 | 9 | public class TestDoubleConverter { 10 | 11 | @Test 12 | public void testShouldConvertToMinValue() { 13 | DefaultConverter converter = new DoubleConverter() 14 | .withFallbackStrategy(FallbackStrategy.DEFAULT_TO_MIN_VALUE); 15 | double val = converter.convert(null, "COLUMN_NAME", 1); 16 | assertEquals(Double.MIN_VALUE, val, 0.0); 17 | 18 | val = converter.convert("", "COLUMN_NAME", 1); 19 | assertEquals(Double.MIN_VALUE, val, 0.0); 20 | 21 | val = converter.convert("not a number", "COLUMN_NAME", 1); 22 | assertEquals(Double.MIN_VALUE, val, 0.0); 23 | 24 | val = converter.convert("1.0", "COLUMN_NAME", 1); 25 | assertNotEquals(Double.MIN_VALUE, val, 0.0); 26 | assertEquals(1.0, val, 0.0); 27 | } 28 | 29 | @Test(expected = IgnoreInvalidValueException.class) 30 | public void testShouldThrowExceptionWhen_DO_NOT_SET_StrategyIsUsed() { 31 | DefaultConverter converter = new DoubleConverter() 32 | .withFallbackStrategy(FallbackStrategy.IGNORE); 33 | 34 | converter.convert(null, "COLUMN_NAME", 1); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/converter/TestLongConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.converter; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNotEquals; 7 | 8 | public class TestLongConverter { 9 | @Test 10 | public void testConvertValuesToLong() { 11 | DefaultConverter converter = new LongConverter() 12 | .withFallbackStrategy(FallbackStrategy.DEFAULT); 13 | 14 | long got = converter.convert("Hello", "A", 1); 15 | assertEquals(Long.MIN_VALUE, got, 0.0); 16 | 17 | } 18 | 19 | /** 20 | * switch (this.getFallbackStrategy()) { 21 | * case LEGACY: 22 | * case DEFAULT_TO_NULL: 23 | * return null; 24 | * case DEFAULT_TO_ZERO: 25 | * return 0L; 26 | * case THROW_EXCEPTION: 27 | * throw new ZeroCellException(String.format(message, value, columnName, row)); 28 | * case IGNORE: 29 | * throw new IgnoreInvalidValueException(); 30 | * case DEFAULT_TO_MIN_VALUE: 31 | * case DEFAULT: 32 | * default: 33 | * return Long.MIN_VALUE; 34 | * } 35 | */ 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/handler/TestColumnFieldWriter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.handler; 2 | 3 | import com.creditdatamw.zerocell.column.ColumnInfo; 4 | import com.creditdatamw.zerocell.converter.FallbackStrategy; 5 | import com.creditdatamw.zerocell.converter.NoopConverter; 6 | import org.junit.Test; 7 | 8 | import java.lang.reflect.Field; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | public class TestColumnFieldWriter { 13 | 14 | @Test 15 | public void testWillNotSetFieldIfDO_NOT_SETStrategyIsUsed() throws NoSuchFieldException { 16 | EntityExcelSheetHandler.ColumnFieldWriter columnFieldWriter = 17 | new EntityExcelSheetHandler.ColumnFieldWriter(); 18 | 19 | Item item = new Item(); 20 | item.fallbackStrategyDoNotSet = 1234567; 21 | Field field = Item.class.getDeclaredField("fallbackStrategyDoNotSet"); 22 | 23 | ColumnInfo columnInfo = new ColumnInfo( 24 | "COLUMN", 25 | "fallbackStrategyDoNotSet", 26 | 0, 27 | Integer.class, 28 | FallbackStrategy.IGNORE 29 | ); 30 | 31 | columnFieldWriter.writeColumnField( 32 | field, 33 | item, 34 | "NOT A NUMBER 999", 35 | columnInfo, 36 | 1, 37 | new NoopConverter() 38 | ); 39 | 40 | assertEquals(1234567, item.getFallbackStrategyDoNotSet()); 41 | } 42 | 43 | private static class Item { 44 | private int fallbackStrategyDoNotSet; 45 | 46 | public int getFallbackStrategyDoNotSet() { 47 | return fallbackStrategyDoNotSet; 48 | } 49 | 50 | public void setFallbackStrategyDoNotSet(int fallbackStrategyDoNotSet) { 51 | this.fallbackStrategyDoNotSet = fallbackStrategyDoNotSet; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/handler/TestEntityExcelSheetHandler.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.handler; 2 | 3 | import com.creditdatamw.zerocell.Person; 4 | import com.creditdatamw.zerocell.column.ColumnMapping; 5 | 6 | /** 7 | * Tests for the EntityExcelSheetHandler 8 | */ 9 | public class TestEntityExcelSheetHandler { 10 | 11 | // @Test 12 | public void testHandlerWithDefaults() { 13 | EntityHandler entityHandler = new EntityHandler<>( 14 | Person.class, 15 | true, 16 | 0, 17 | 0 18 | ); 19 | 20 | ColumnMapping mapping = ColumnMapping.parseColumnMappingFromAnnotations(Person.class); 21 | EntityExcelSheetHandler sheetHandler = 22 | new EntityExcelSheetHandler( 23 | entityHandler, 24 | mapping.getRowNumberInfo(), 25 | mapping.getColumnsMap() 26 | ); 27 | // TODO: test the sheet handler's methods 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/test/java/com/creditdatamw/zerocell/handler/TestEntityHandler.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.handler; 2 | 3 | import com.creditdatamw.zerocell.Person; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertTrue; 8 | 9 | public class TestEntityHandler { 10 | 11 | @Test 12 | public void testHandlerWithDefaults() { 13 | EntityHandler entityHandler = new EntityHandler<>( 14 | Person.class, 15 | true, 16 | 0, 17 | 0 18 | ); 19 | 20 | assertEquals(Person.class, entityHandler.getEntityClass()); 21 | assertEquals("ZEROCELL_READ_FIRST_SHEET_0", entityHandler.getSheetName()); 22 | assertTrue(entityHandler.isSkipHeaderRow()); 23 | assertEquals(0, entityHandler.getSkipFirstNRows()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/resources/test_people.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creditdatamw/zerocell/8dd99a10d9890a575a95a5b9ba17d70ac3cfe3b5/core/src/test/resources/test_people.xlsx -------------------------------------------------------------------------------- /core/src/test/resources/test_people_with_empty_rows.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creditdatamw/zerocell/8dd99a10d9890a575a95a5b9ba17d70ac3cfe3b5/core/src/test/resources/test_people_with_empty_rows.xlsx -------------------------------------------------------------------------------- /core/src/test/resources/test_people_with_offset_header.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creditdatamw/zerocell/8dd99a10d9890a575a95a5b9ba17d70ac3cfe3b5/core/src/test/resources/test_people_with_offset_header.xlsx -------------------------------------------------------------------------------- /core/src/test/resources/test_people_with_offset_sheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creditdatamw/zerocell/8dd99a10d9890a575a95a5b9ba17d70ac3cfe3b5/core/src/test/resources/test_people_with_offset_sheet.xlsx -------------------------------------------------------------------------------- /example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zerocell-parent 5 | com.creditdatamw.labs 6 | 0.5.2-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | 11 | zerocell-example 12 | 13 | 14 | 15 | com.creditdatamw.labs 16 | zerocell-core 17 | ${project.version} 18 | 19 | 20 | com.creditdatamw.labs 21 | zerocell-processor 22 | ${project.version} 23 | provided 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.codehaus.mojo 31 | exec-maven-plugin 32 | 33 | 34 | 35 | exec 36 | 37 | 38 | 39 | 40 | java 41 | 42 | -classpath 43 | 44 | com.creditdatamw.zerocell.example.BasicUsageExample 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-resources-plugin 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-compiler-plugin 55 | 56 | 1.8 57 | 1.8 58 | UTF-8 59 | src/generated/java 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-jar-plugin 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-clean-plugin 69 | 70 | 71 | 72 | ${basedir}/src/generated 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /example/src/generated/java/com/creditdatamw/zerocell/example/PersonExcelReader.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import static com.creditdatamw.zerocell.converter.Converters.*; 4 | 5 | import com.creditdatamw.zerocell.ReaderUtil; 6 | import com.creditdatamw.zerocell.ZeroCellException; 7 | import com.creditdatamw.zerocell.ZeroCellReader; 8 | import java.io.File; 9 | import java.lang.Override; 10 | import java.lang.String; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import org.apache.poi.ss.util.CellAddress; 15 | import org.apache.poi.ss.util.CellReference; 16 | import org.apache.poi.xssf.usermodel.XSSFComment; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | public final class PersonExcelReader implements ZeroCellReader { 21 | private static Logger LOGGER = LoggerFactory.getLogger(com.creditdatamw.zerocell.example.Person.class); 22 | 23 | static final int COL_0 = 0; 24 | 25 | static final int COL_1 = 1; 26 | 27 | static final int COL_2 = 2; 28 | 29 | static final int COL_3 = 3; 30 | 31 | static final int COL_4 = 4; 32 | 33 | static final int COL_6 = 6; 34 | 35 | static final int COL_5 = 5; 36 | 37 | private boolean validateHeaders; 38 | 39 | private boolean isHeaderRow; 40 | 41 | private int currentRow; 42 | 43 | private int currentCol; 44 | 45 | private Person cur; 46 | 47 | private List data; 48 | 49 | public PersonExcelReader() { 50 | this.data = new ArrayList<>(); 51 | } 52 | 53 | @Override 54 | public List read(final File file, final String sheet) { 55 | this.reset(); 56 | ReaderUtil.process(file, sheet, this); 57 | List dataList; 58 | dataList = Collections.unmodifiableList(this.data); 59 | return dataList; 60 | } 61 | 62 | private void reset() { 63 | this.currentRow = -1; 64 | this.currentCol = -1; 65 | this.cur = null; 66 | this.data.clear(); 67 | } 68 | 69 | @Override 70 | public void headerFooter(final String text, final boolean b, final String tagName) { 71 | // Skip, not processing headers or footers here 72 | } 73 | 74 | @Override 75 | public void startRow(final int i) { 76 | currentRow = i; 77 | isHeaderRow = false; 78 | // Skip header row 79 | if (currentRow == 0) { 80 | isHeaderRow=true; 81 | return; 82 | } 83 | cur = new Person(); 84 | cur.setRow(currentRow); 85 | } 86 | 87 | @Override 88 | public void cell(String cellReference, final String formattedValue, 89 | final XSSFComment xssfComment) { 90 | if (java.util.Objects.isNull(cur)) return; 91 | // Gracefully handle missing CellRef here in a similar way as XSSFCell does 92 | if(cellReference == null) { 93 | cellReference = new CellAddress(currentRow, currentCol).formatAsString(); 94 | } 95 | int column = new CellReference(cellReference).getCol(); 96 | currentCol = column; 97 | if (COL_0 == column) { 98 | assertColumnName("ID", formattedValue); 99 | //Handle any exceptions thrown by the converter - this stops execution of the whole process; 100 | try { 101 | cur.setId(new com.creditdatamw.zerocell.example.IdPrefixingConverter().convert(formattedValue, "ID", currentRow)); 102 | } catch(Exception e) { 103 | throw new ZeroCellException("com.creditdatamw.zerocell.example.IdPrefixingConverter threw an exception while trying to convert value " + formattedValue, e); 104 | } 105 | return; 106 | } 107 | if (COL_1 == column) { 108 | assertColumnName("FIRST_NAME", formattedValue); 109 | cur.setFirstName(noop.convert(formattedValue, "FIRST_NAME", currentRow)); 110 | return; 111 | } 112 | if (COL_2 == column) { 113 | assertColumnName("MIDDLE_NAME", formattedValue); 114 | cur.setMiddleName(noop.convert(formattedValue, "MIDDLE_NAME", currentRow)); 115 | return; 116 | } 117 | if (COL_3 == column) { 118 | assertColumnName("LAST_NAME", formattedValue); 119 | cur.setLastName(noop.convert(formattedValue, "LAST_NAME", currentRow)); 120 | return; 121 | } 122 | if (COL_4 == column) { 123 | assertColumnName("DATE_OF_BIRTH", formattedValue); 124 | cur.setDateOfBirth(toLocalDate.convert(formattedValue, "DATE_OF_BIRTH", currentRow)); 125 | return; 126 | } 127 | if (COL_6 == column) { 128 | assertColumnName("DATE_REGISTERED", formattedValue); 129 | cur.setDateOfRegistration(toLocalDate.convert(formattedValue, "DATE_REGISTERED", currentRow)); 130 | return; 131 | } 132 | if (COL_5 == column) { 133 | assertColumnName("FAV_NUMBER", formattedValue); 134 | cur.setFavouriteNumber(toInteger.convert(formattedValue, "FAV_NUMBER", currentRow)); 135 | return; 136 | } 137 | } 138 | 139 | @Override 140 | public void endRow(final int i) { 141 | if (! java.util.Objects.isNull(cur)) { 142 | this.data.add(cur); 143 | this.cur = null; 144 | } 145 | } 146 | 147 | void assertColumnName(final String columnName, final String value) { 148 | if (validateHeaders && isHeaderRow) { 149 | if (! columnName.equalsIgnoreCase(value)) { 150 | throw new ZeroCellException(String.format("Expected Column '%s' but found '%s'", columnName, value)); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/AnnotationProcessorExample.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import com.creditdatamw.zerocell.ZeroCellException; 4 | import com.creditdatamw.zerocell.ZeroCellReader; 5 | 6 | import java.io.File; 7 | import java.util.List; 8 | 9 | /** 10 | * This example shows how to load rows from an Excel Sheet into a List via the 11 | * class generated by the zerocell annotation processor. 12 | * 13 | * The class can be found when you build the module in the target/classes/generated 14 | * directory. 15 | * 16 | * The API for this is also very simple and straight forward. 17 | */ 18 | public class AnnotationProcessorExample { 19 | /** 20 | * MainEntity method 21 | * 22 | * @param args 23 | * @throws ZeroCellException 24 | */ 25 | public static void main(String... args) throws ZeroCellException { 26 | File file = new File("../core/src/test/resources/test_people.xlsx"); 27 | 28 | String sheet = "uploads"; 29 | 30 | ZeroCellReader reader = new PersonExcelReader(); 31 | 32 | List people = reader.read(file, sheet); 33 | 34 | System.out.println("People from the file:"); 35 | 36 | for(Person p: people) { 37 | System.out.println(String.format("row:%s data: %s", p.getRow(), p.toString())); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/BasicUsageExample.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import com.creditdatamw.zerocell.Reader; 4 | import com.creditdatamw.zerocell.ReaderUtil; 5 | import com.creditdatamw.zerocell.ZeroCellException; 6 | import com.creditdatamw.zerocell.ZeroCellReader; 7 | 8 | import java.io.File; 9 | import java.util.List; 10 | 11 | /** 12 | * This Basic Example shows how to load rows from an Excel Sheet into a List. 13 | * As you can see, it's really straight forward to read the rows. 14 | * 15 | */ 16 | public class BasicUsageExample { 17 | /** 18 | * MainEntity method 19 | * 20 | * @param args 21 | * @throws ZeroCellException 22 | */ 23 | public static void main(String... args) throws ZeroCellException { 24 | File file = new File("../core/src/test/resources/test_people.xlsx"); 25 | 26 | String sheet = "uploads"; 27 | 28 | List people = Reader.of(Person.class) 29 | .from(file) 30 | .sheet(sheet) 31 | .list(); 32 | 33 | System.out.println("People from the file:"); 34 | 35 | for(Person p: people) { 36 | System.out.println(String.format("row:%s data: %s", p.getRow(), p.toString())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/IdPrefixingConverter.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import com.creditdatamw.zerocell.converter.Converter; 4 | 5 | /** 6 | * Simple Converter that prefixes values with ID- 7 | */ 8 | public class IdPrefixingConverter implements Converter { 9 | @Override 10 | public String convert(String value, String columnName, int row) { 11 | return String.format("ID-%s", value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/ManualColumnMappingExample.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import com.creditdatamw.zerocell.Reader; 4 | import com.creditdatamw.zerocell.ZeroCellException; 5 | import com.creditdatamw.zerocell.column.ColumnInfo; 6 | import com.creditdatamw.zerocell.column.RowNumberInfo; 7 | 8 | import java.io.File; 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | /** 13 | * This example shows how to load data from an excel sheet without using annotations 14 | * on the class itself. The column mappings are via the Reader.using method. 15 | * 16 | * Also note that zerocell doesn't require that ALL columns in the Excel sheet be 17 | * mapped to process the sheet. 18 | * 19 | */ 20 | public class ManualColumnMappingExample { 21 | /** 22 | * MainEntity method 23 | * 24 | * @param args 25 | * @throws ZeroCellException 26 | */ 27 | public static void main(String... args) throws ZeroCellException { 28 | File file = new File("../core/src/test/resources/test_people.xlsx"); 29 | 30 | String sheet = "uploads"; 31 | 32 | List patients = Reader.of(Patient.class) 33 | .from(file) 34 | .sheet(sheet) 35 | .using( 36 | new RowNumberInfo("row", Integer.class), 37 | new ColumnInfo("ID", "patientID", 0, String.class), 38 | new ColumnInfo("FIRST_NAME", "forename", 1, String.class), 39 | new ColumnInfo("MIDDLE_NAME", "otherNames", 2, String.class), 40 | new ColumnInfo("LAST_NAME", "surname", 3, String.class), 41 | new ColumnInfo("DATE_OF_BIRTH", "dateOfBirth", 4, LocalDate.class) 42 | ) 43 | .list(); 44 | 45 | System.out.println("Patients from the file:"); 46 | 47 | for(Patient p: patients) { 48 | System.out.println(String.format("row:%s data: %s", p.getRow(), p.toString())); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/Patient.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class Patient { 6 | private int row; 7 | 8 | private String patientID; 9 | 10 | private String forename; 11 | 12 | private String otherNames; 13 | 14 | private String surname; 15 | 16 | private LocalDate dateOfBirth; 17 | 18 | private LocalDate dateAdmitted; 19 | 20 | public int getRow() { 21 | return row; 22 | } 23 | 24 | public void setRow(int row) { 25 | this.row = row; 26 | } 27 | 28 | public String getPatientID() { 29 | return patientID; 30 | } 31 | 32 | public void setPatientID(String patientID) { 33 | this.patientID = patientID; 34 | } 35 | 36 | public String getForename() { 37 | return forename; 38 | } 39 | 40 | public void setForename(String forename) { 41 | this.forename = forename; 42 | } 43 | 44 | public String getOtherNames() { 45 | return otherNames; 46 | } 47 | 48 | public void setOtherNames(String otherNames) { 49 | this.otherNames = otherNames; 50 | } 51 | 52 | public String getSurname() { 53 | return surname; 54 | } 55 | 56 | public void setSurname(String surname) { 57 | this.surname = surname; 58 | } 59 | 60 | public LocalDate getDateOfBirth() { 61 | return dateOfBirth; 62 | } 63 | 64 | public void setDateOfBirth(LocalDate dateOfBirth) { 65 | this.dateOfBirth = dateOfBirth; 66 | } 67 | 68 | public LocalDate getDateAdmitted() { 69 | return dateAdmitted; 70 | } 71 | 72 | public void setDateAdmitted(LocalDate dateAdmitted) { 73 | this.dateAdmitted = dateAdmitted; 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | return "Patient{" + 79 | "row=" + row + 80 | ", patientID='" + patientID + '\'' + 81 | ", forename='" + forename + '\'' + 82 | ", otherNames='" + otherNames + '\'' + 83 | ", surname='" + surname + '\'' + 84 | ", dateOfBirth=" + dateOfBirth + 85 | ", dateAdmitted=" + dateAdmitted + 86 | '}'; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/src/main/java/com/creditdatamw/zerocell/example/Person.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.example; 2 | 3 | import com.creditdatamw.zerocell.annotation.Column; 4 | import com.creditdatamw.zerocell.annotation.RowNumber; 5 | import com.creditdatamw.zerocell.annotation.ZerocellReaderBuilder; 6 | 7 | import java.time.LocalDate; 8 | 9 | /** 10 | * Person Class for tests 11 | */ 12 | @ZerocellReaderBuilder("PersonExcelReader") 13 | public class Person { 14 | @RowNumber 15 | private int row; 16 | 17 | @Column(name= "ID", index=0, converterClass = IdPrefixingConverter.class) 18 | private String id; 19 | 20 | @Column(name = "FIRST_NAME", index = 1) 21 | private String firstName; 22 | 23 | @Column(name = "MIDDLE_NAME", index = 2) 24 | private String middleName; 25 | 26 | @Column(name = "LAST_NAME", index = 3) 27 | private String lastName; 28 | 29 | @Column(name = "DATE_OF_BIRTH", index = 4) 30 | private LocalDate dateOfBirth; 31 | 32 | @Column(name = "DATE_REGISTERED", index = 6) 33 | private LocalDate dateOfRegistration; 34 | 35 | @Column(name = "FAV_NUMBER", index = 5) 36 | private int favouriteNumber; 37 | 38 | public Person() { 39 | } 40 | 41 | public int getRow() { 42 | return row; 43 | } 44 | 45 | public void setRow(int row) { 46 | this.row = row; 47 | } 48 | 49 | public String getId() { 50 | return id; 51 | } 52 | 53 | public void setId(String id) { 54 | this.id = id; 55 | } 56 | 57 | public String getFirstName() { 58 | return firstName; 59 | } 60 | 61 | public void setFirstName(String firstName) { 62 | this.firstName = firstName; 63 | } 64 | 65 | public String getMiddleName() { 66 | return middleName; 67 | } 68 | 69 | public void setMiddleName(String middleName) { 70 | this.middleName = middleName; 71 | } 72 | 73 | public String getLastName() { 74 | return lastName; 75 | } 76 | 77 | public void setLastName(String lastName) { 78 | this.lastName = lastName; 79 | } 80 | 81 | public LocalDate getDateOfBirth() { 82 | return dateOfBirth; 83 | } 84 | 85 | public void setDateOfBirth(LocalDate dateOfBirth) { 86 | this.dateOfBirth = dateOfBirth; 87 | } 88 | 89 | public LocalDate getDateOfRegistration() { 90 | return dateOfRegistration; 91 | } 92 | 93 | public void setDateOfRegistration(LocalDate dateOfRegistration) { 94 | this.dateOfRegistration = dateOfRegistration; 95 | } 96 | 97 | public int getFavouriteNumber() { 98 | return favouriteNumber; 99 | } 100 | 101 | public void setFavouriteNumber(int favouriteNumber) { 102 | this.favouriteNumber = favouriteNumber; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.creditdatamw.labs 7 | zerocell-parent 8 | 9 | 0.5.2-SNAPSHOT 10 | 11 | pom 12 | https://github.com/creditdatamw/zerocell 13 | 14 | 15 | 16 | The Apache Software License, Version 2.0 17 | http://www.apache.org/licenses/LICENSE-2.0.txt 18 | 19 | 20 | 21 | 22 | processor 23 | core 24 | example 25 | 26 | 27 | ZeroCell 28 | Read from Excel files to POJO's really fast! 29 | 30 | 31 | 5.1.0 32 | UTF-8 33 | 34 | 35 | 36 | Credit Data CRB Ltd 37 | it@creditdatamw.com 38 | Africa/Harare 39 | 40 | owner 41 | 42 | 43 | 44 | 45 | scm:git:git://github.com/creditdatamw/zerocell.git 46 | scm:git:git@github.com:creditdatamw/zerocell.git 47 | https://github.com/creditdatamw/zerocell 48 | master 49 | 50 | 51 | GitHub 52 | https://github.com/creditdatamw/zerocell/issues 53 | 54 | 55 | 56 | ossrh 57 | https://oss.sonatype.org/content/repositories/snapshots 58 | 59 | 60 | ossrh 61 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 62 | 63 | 64 | 65 | 66 | org.apache.poi 67 | poi 68 | ${poi.version} 69 | 70 | 71 | org.apache.poi 72 | poi-ooxml 73 | ${poi.version} 74 | 75 | 76 | org.slf4j 77 | slf4j-api 78 | 1.7.25 79 | 80 | 81 | ch.qos.logback 82 | logback-classic 83 | 1.2.0 84 | test 85 | 86 | 87 | junit 88 | junit 89 | 4.13.1 90 | test 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.apache.poi 98 | poi 99 | ${poi.version} 100 | 101 | 102 | org.apache.poi 103 | poi-ooxml 104 | ${poi.version} 105 | 106 | 107 | org.apache.xmlgraphics 108 | batik-all 109 | 110 | 111 | 112 | 113 | org.slf4j 114 | slf4j-api 115 | 1.7.25 116 | 117 | 118 | com.squareup 119 | javapoet 120 | 1.8.0 121 | 122 | 123 | junit 124 | junit 125 | 4.13.1 126 | test 127 | 128 | 129 | 130 | 131 | 132 | 133 | release 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-release-plugin 139 | 2.5.3 140 | 141 | true 142 | false 143 | release 144 | deploy 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-gpg-plugin 150 | 1.5 151 | 152 | 153 | sign-artifacts 154 | verify 155 | 156 | sign 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-resources-plugin 171 | 3.0.2 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-clean-plugin 176 | 3.0.0 177 | 178 | 179 | org.apache.maven.plugins 180 | maven-surefire-plugin 181 | 2.19.1 182 | 183 | 184 | maven-compiler-plugin 185 | 3.5.1 186 | 187 | 1.8 188 | 1.8 189 | UTF-8 190 | 191 | 192 | 193 | com.github.spotbugs 194 | spotbugs-maven-plugin 195 | 4.2.0 196 | 197 | Max 198 | Default 199 | true 200 | ${basedir}/../findbugs-exclude.xml 201 | 202 | 203 | 204 | 205 | compile 206 | 207 | 208 | check 209 | 210 | 211 | 212 | 213 | 214 | org.apache.maven.plugins 215 | maven-source-plugin 216 | 3.0.1 217 | 218 | 219 | attach-sources 220 | 221 | jar-no-fork 222 | 223 | 224 | 225 | 226 | 227 | org.apache.maven.plugins 228 | maven-javadoc-plugin 229 | 3.0.0-M1 230 | 231 | 232 | attach-javadocs 233 | 234 | jar 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | maven-compiler-plugin 245 | 3.5.1 246 | 247 | 1.8 248 | 1.8 249 | UTF-8 250 | 251 | 252 | 253 | org.codehaus.mojo 254 | exec-maven-plugin 255 | 1.5.0 256 | 257 | 258 | org.apache.maven.plugins 259 | maven-jar-plugin 260 | 3.0.2 261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /processor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zerocell-parent 5 | com.creditdatamw.labs 6 | 7 | 0.5.2-SNAPSHOT 8 | 9 | 10 | 4.0.0 11 | 12 | zerocell-processor 13 | 14 | 15 | 16 | com.creditdatamw.labs 17 | zerocell-core 18 | ${project.version} 19 | 20 | 21 | com.squareup 22 | javapoet 23 | 24 | 25 | javax.annotation 26 | javax.annotation-api 27 | 1.3 28 | true 29 | 30 | 31 | 32 | com.google.auto.service 33 | auto-service 34 | 1.0-rc3 35 | true 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-resources-plugin 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-compiler-plugin 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-jar-plugin 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /processor/src/main/java/com/creditdatamw/zerocell/processor/ZeroCellAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.processor; 2 | 3 | import com.creditdatamw.zerocell.annotation.Column; 4 | import com.creditdatamw.zerocell.annotation.RowNumber; 5 | import com.creditdatamw.zerocell.annotation.ZerocellReaderBuilder; 6 | import com.creditdatamw.zerocell.processor.spec.ReaderTypeSpec; 7 | import com.google.auto.service.AutoService; 8 | import com.squareup.javapoet.JavaFile; 9 | 10 | import javax.annotation.processing.*; 11 | import javax.lang.model.SourceVersion; 12 | import javax.lang.model.element.Element; 13 | import javax.lang.model.element.ElementKind; 14 | import javax.lang.model.element.TypeElement; 15 | import javax.lang.model.util.Elements; 16 | import javax.lang.model.util.Types; 17 | import javax.tools.Diagnostic; 18 | import java.util.HashSet; 19 | import java.util.Optional; 20 | import java.util.Set; 21 | 22 | /** 23 | * ZeroCell Annotation Processor - generates classes that read values from Excel 24 | * rows into POJOs using the Excel SAX Handler approach which is much more efficient 25 | * at than the User model classes such as Cell. 26 | * 27 | * @author Zikani Nyirenda Mwase 28 | */ 29 | @AutoService(Processor.class) 30 | public class ZeroCellAnnotationProcessor extends AbstractProcessor { 31 | private Types typeUtils; 32 | private Elements elementUtils; 33 | private Filer filer; 34 | private Messager messager; 35 | 36 | @SuppressWarnings("unused") 37 | public ZeroCellAnnotationProcessor() { 38 | } 39 | 40 | @Override 41 | public Set getSupportedAnnotationTypes() { 42 | Set annotations = new HashSet<>(); 43 | annotations.add(ZerocellReaderBuilder.class.getCanonicalName()); 44 | annotations.add(Column.class.getCanonicalName()); 45 | annotations.add(RowNumber.class.getCanonicalName()); 46 | return annotations; 47 | } 48 | 49 | @Override 50 | public SourceVersion getSupportedSourceVersion() { 51 | return SourceVersion.latestSupported(); 52 | } 53 | 54 | @Override 55 | public synchronized void init(ProcessingEnvironment env) { 56 | super.init(env); 57 | typeUtils = env.getTypeUtils(); 58 | elementUtils = env.getElementUtils(); 59 | filer = env.getFiler(); 60 | messager = env.getMessager(); 61 | } 62 | 63 | @Override 64 | public boolean process(Set annotations, RoundEnvironment env) { 65 | for(Element annotatedElement : env.getElementsAnnotatedWith(ZerocellReaderBuilder.class)) { 66 | if (annotatedElement.getKind() != ElementKind.CLASS) { 67 | env.errorRaised(); 68 | return true; 69 | } 70 | TypeElement classElement = (TypeElement) annotatedElement; 71 | try { 72 | ZerocellReaderBuilder annotation = annotatedElement.getAnnotation(ZerocellReaderBuilder.class); 73 | Optional customReaderName = Optional.empty(); 74 | String name = annotation.value(); 75 | if (!name.equals("__none__") && !name.isEmpty()) { 76 | customReaderName = Optional.of(name); 77 | } 78 | ReaderTypeSpec spec = new ReaderTypeSpec(classElement, customReaderName); 79 | JavaFile javaFile = spec.build(); 80 | javaFile.writeTo(filer); 81 | } catch (Exception ioe) { 82 | ioe.printStackTrace(); 83 | messager.printMessage(Diagnostic.Kind.ERROR, 84 | String.format("Failed to generate the Reader class for %s, got exception: %s", 85 | classElement.getQualifiedName().toString(), 86 | ioe.getMessage()), 87 | annotatedElement); 88 | env.errorRaised(); 89 | return true; 90 | } 91 | } 92 | return false; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /processor/src/main/java/com/creditdatamw/zerocell/processor/spec/CellMethodSpec.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.processor.spec; 2 | 3 | import com.creditdatamw.zerocell.converter.NoopConverter; 4 | import com.creditdatamw.zerocell.processor.ZeroCellAnnotationProcessor; 5 | import com.squareup.javapoet.CodeBlock; 6 | import com.squareup.javapoet.MethodSpec; 7 | import org.apache.poi.xssf.usermodel.XSSFComment; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import javax.lang.model.element.Modifier; 11 | import java.sql.Timestamp; 12 | import java.time.LocalDate; 13 | import java.time.LocalDateTime; 14 | import java.util.List; 15 | 16 | /** 17 | * Generates a method that uses a specified convertor 18 | * for a field in a ZerocellReaderBuilder. 19 | */ 20 | public class CellMethodSpec { 21 | 22 | public static MethodSpec build(List columns) { 23 | final CodeBlock.Builder builder = CodeBlock.builder(); 24 | LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class) 25 | .info("Found {} columns in source class", columns.size()); 26 | columns.forEach(column -> { 27 | String staticFieldName = "COL_" + column.getIndex(); 28 | String fieldName = column.getFieldName(); 29 | 30 | String beanSetterProperty = beanSetterPropertyName(fieldName); 31 | 32 | builder.beginControlFlow("if ($L == column)", staticFieldName) 33 | .addStatement("assertColumnName($S, formattedValue)", column.getName()); 34 | 35 | converterStatementFor(builder, column, beanSetterProperty); 36 | 37 | builder.addStatement("return").endControlFlow(); 38 | }); 39 | 40 | return MethodSpec.methodBuilder("cell") 41 | .addAnnotation(Override.class) 42 | .addModifiers(Modifier.PUBLIC) 43 | .addParameter(String.class, "cellReference") 44 | .addParameter(String.class, "formattedValue", Modifier.FINAL) 45 | .addParameter(XSSFComment.class, "xssfComment", Modifier.FINAL) 46 | .addStatement("if (java.util.Objects.isNull(cur)) return") 47 | .addComment("Gracefully handle missing CellRef here in a similar way as XSSFCell does") 48 | .beginControlFlow("if(cellReference == null)") 49 | .addStatement("cellReference = new $T(currentRow, currentCol).formatAsString()", org.apache.poi.ss.util.CellAddress.class) 50 | .endControlFlow() 51 | .addStatement("int column = new $T(cellReference).getCol()", org.apache.poi.ss.util.CellReference.class) 52 | .addStatement("currentCol = column") 53 | .addCode(builder.build()) 54 | .build(); 55 | } 56 | 57 | private static void converterStatementFor(CodeBlock.Builder builder, 58 | ColumnInfoType column, 59 | String beanSetterProperty) { 60 | String columnName = column.getName(); 61 | String converterClass = String.format("%s", column.getConverterClass()); 62 | String throwException = "throw new ZeroCellException(\"$L threw an exception while trying to convert value \" + formattedValue, e)"; 63 | 64 | CodeBlock converterCodeBlock = CodeBlock.builder() 65 | .addStatement("//Handle any exceptions thrown by the converter - this stops execution of the whole process") 66 | .beginControlFlow("try") 67 | .addStatement("cur.set$L(new $L().convert(formattedValue, $S, currentRow))", 68 | beanSetterProperty, 69 | converterClass, 70 | columnName) 71 | .nextControlFlow("catch(Exception e)") 72 | .addStatement(throwException, converterClass) 73 | .endControlFlow() 74 | .build(); 75 | // Don't use a converter if there isn't a custom one 76 | if (converterClass.equals(NoopConverter.class.getTypeName())) { 77 | builder.addStatement(converterStatementFor(column), beanSetterProperty, columnName); 78 | } else { 79 | builder.add(converterCodeBlock); 80 | } 81 | } 82 | 83 | public static String converterStatementFor(ColumnInfoType column) { 84 | String fieldType = String.format("%s", column.getType()); 85 | 86 | if (fieldType.equals(LocalDateTime.class.getTypeName())) { 87 | return "cur.set$L(toLocalDateTime.convert(formattedValue, $S, currentRow))"; 88 | 89 | } else if (fieldType.equals(LocalDate.class.getTypeName())) { 90 | return "cur.set$L(toLocalDate.convert(formattedValue, $S, currentRow))"; 91 | 92 | } else if (fieldType.equals(java.sql.Date.class.getTypeName())) { 93 | return "cur.set$L(toSqlDate.convert(formattedValue, $S, currentRow))"; 94 | 95 | } else if (fieldType.equals(Timestamp.class.getTypeName())) { 96 | return "cur.set$L(toSqlTimestamp.convert(formattedValue, $S, currentRow))"; 97 | 98 | } else if (fieldType.equals(Integer.class.getTypeName()) || 99 | fieldType.equals(int.class.getTypeName())) { 100 | return "cur.set$L(toInteger.convert(formattedValue, $S, currentRow))"; 101 | 102 | } else if (fieldType.equals(Long.class.getTypeName()) || 103 | fieldType.equals(long.class.getTypeName())) { 104 | return "cur.set$L(toLong.convert(formattedValue, $S, currentRow))"; 105 | 106 | } else if (fieldType.equals(Double.class.getTypeName()) || 107 | fieldType.equals(double.class.getTypeName())) { 108 | return "cur.set$L(toDouble.convert(formattedValue, $S, currentRow))"; 109 | 110 | } else if (fieldType.equals(Float.class.getTypeName()) || 111 | fieldType.equals(float.class.getTypeName())) { 112 | return "cur.set$L(toFloat.convert(formattedValue, $S, currentRow))"; 113 | 114 | } else if (fieldType.equals(Boolean.class.getTypeName())) { 115 | return "cur.set$L(toBoolean.convert(formattedValue, $S, currentRow))"; 116 | } 117 | // Default to Converters.noop.convert 118 | return "cur.set$L(noop.convert(formattedValue, $S, currentRow))"; 119 | } 120 | 121 | /** 122 | * Returns the field name for a "setter" for a POJO, i.e. the part after the "set" text. 123 | *
124 | * Example: "set" + beanSetterPropertyName("name") -> "setName" 125 | * 126 | * @param fieldName 127 | * @return 128 | */ 129 | static String beanSetterPropertyName(String fieldName) { 130 | // TODO(zikani): Find a better way ?? 131 | // Here we're assuming people follow standards of naming POJO fields 132 | String beanSetterProperty = String.valueOf(fieldName.charAt(0)) 133 | .toUpperCase() 134 | .concat(fieldName.substring(1)); 135 | return beanSetterProperty; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /processor/src/main/java/com/creditdatamw/zerocell/processor/spec/ColumnInfoType.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.processor.spec; 2 | 3 | import com.creditdatamw.zerocell.ZeroCellException; 4 | import com.creditdatamw.zerocell.annotation.Column; 5 | 6 | import javax.lang.model.element.Element; 7 | import javax.lang.model.element.TypeElement; 8 | import javax.lang.model.type.MirroredTypeException; 9 | import javax.lang.model.type.TypeMirror; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | /** 15 | * Custom "type" class representing a column 16 | */ 17 | class ColumnInfoType { 18 | private String name; 19 | 20 | private String fieldName; 21 | 22 | private int index; 23 | 24 | private String dataFormat; 25 | 26 | private TypeMirror type; 27 | 28 | private TypeMirror converterClass; 29 | 30 | public ColumnInfoType(String name, String fieldName, int index, String dataFormat, TypeMirror type, TypeMirror converterClass) { 31 | this.name = name; 32 | this.fieldName = fieldName; 33 | this.index = index; 34 | this.dataFormat = dataFormat; 35 | this.type = type; 36 | this.converterClass = converterClass; 37 | } 38 | 39 | public String getName() { 40 | return name.toUpperCase().trim(); 41 | } 42 | 43 | public void setName(String name) { 44 | this.name = name; 45 | } 46 | 47 | public String getFieldName() { 48 | return fieldName; 49 | } 50 | 51 | public void setFieldName(String fieldName) { 52 | this.fieldName = fieldName; 53 | } 54 | 55 | public int getIndex() { 56 | return index; 57 | } 58 | 59 | public void setIndex(int index) { 60 | this.index = index; 61 | } 62 | 63 | public String getDataFormat() { 64 | return dataFormat; 65 | } 66 | 67 | public void setDataFormat(String dataFormat) { 68 | this.dataFormat = dataFormat; 69 | } 70 | 71 | public TypeMirror getType() { 72 | return type; 73 | } 74 | 75 | public void setType(TypeMirror type) { 76 | this.type = type; 77 | } 78 | 79 | public TypeMirror getConverterClass() { 80 | return converterClass; 81 | } 82 | 83 | public void setConverterClass(TypeMirror converterClass) { 84 | this.converterClass = converterClass; 85 | } 86 | 87 | /** 88 | * Extracts the @{@link Column} annotations from the typeElement 89 | * 90 | * @param typeElement Element of {@link javax.lang.model.element.ElementKind#CLASS} 91 | * @return 92 | */ 93 | public static final List columnsOf(TypeElement typeElement) { 94 | List columns = new ArrayList<>(); 95 | for(Element element: typeElement.getEnclosedElements()) { 96 | if (! element.getKind().isField()) { 97 | continue; 98 | } 99 | Column annotation = element.getAnnotation(Column.class); 100 | if (! Objects.isNull(annotation)) { 101 | TypeMirror typeClazz = null, 102 | converterClazz = null; 103 | try { 104 | typeClazz = element.asType(); 105 | } catch (MirroredTypeException mte) { 106 | typeClazz = mte.getTypeMirror(); 107 | } 108 | try { 109 | annotation.converterClass(); 110 | } catch (MirroredTypeException mte) { 111 | converterClazz = mte.getTypeMirror(); 112 | } 113 | columns.add(new ColumnInfoType(annotation.name(), 114 | element.getSimpleName().toString(), 115 | annotation.index(), 116 | annotation.dataFormat(), 117 | typeClazz, 118 | converterClazz)); 119 | } 120 | } 121 | // Check for out of range and duplicate column indexes 122 | boolean[] indexUsed = new boolean[columns.size()]; 123 | int index; 124 | for(ColumnInfoType c: columns) { 125 | index = c.getIndex(); 126 | if (index > indexUsed.length - 1) { 127 | throw new ZeroCellException( 128 | String.format( 129 | "Column index out of range. index=%s columnCount=%s." + 130 | "Ensure there @Column annotations for all indexes from 0 to %s", 131 | index, 132 | indexUsed.length, 133 | indexUsed.length - 1)); 134 | } 135 | if (indexUsed[index]) { 136 | throw new ZeroCellException("Cannot map two columns to the same index: " + index); 137 | } 138 | indexUsed[index] = true; 139 | } 140 | indexUsed = null; 141 | return columns; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /processor/src/main/java/com/creditdatamw/zerocell/processor/spec/ReaderTypeSpec.java: -------------------------------------------------------------------------------- 1 | package com.creditdatamw.zerocell.processor.spec; 2 | 3 | import com.creditdatamw.zerocell.ZeroCellException; 4 | import com.creditdatamw.zerocell.ZeroCellReader; 5 | import com.creditdatamw.zerocell.annotation.RowNumber; 6 | import com.creditdatamw.zerocell.processor.ZeroCellAnnotationProcessor; 7 | import com.squareup.javapoet.*; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.lang.model.element.Element; 12 | import javax.lang.model.element.Modifier; 13 | import javax.lang.model.element.TypeElement; 14 | import javax.lang.model.type.MirroredTypeException; 15 | import javax.lang.model.type.TypeMirror; 16 | import java.util.*; 17 | import java.util.regex.Pattern; 18 | 19 | import static com.creditdatamw.zerocell.processor.spec.CellMethodSpec.beanSetterPropertyName; 20 | import static com.creditdatamw.zerocell.processor.spec.ColumnInfoType.columnsOf; 21 | 22 | public class ReaderTypeSpec { 23 | private final TypeElement typeElement; 24 | private final String readerClassName; 25 | 26 | private static final String INVALID_CHARS_REGEX = "\\s"; 27 | 28 | public ReaderTypeSpec(TypeElement typeElement, Optional customReaderName) { 29 | Objects.requireNonNull(typeElement); 30 | this.typeElement = typeElement; 31 | this.readerClassName = customReaderName.orElse( 32 | String.format("%sReader", typeElement.getSimpleName())); 33 | } 34 | 35 | public JavaFile build() throws java.io.IOException { 36 | assertReaderName(); 37 | LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class) 38 | .info("Processing class: {}", typeElement); 39 | ClassName dataClass = ClassName.get(typeElement); 40 | ClassName list = ClassName.get("java.util", "List"); 41 | ClassName readerUtil = ClassName.get("com.creditdatamw.zerocell", "ReaderUtil"); 42 | ClassName arrayList = ClassName.get("java.util", "ArrayList"); 43 | ClassName convertersClass = ClassName.get("com.creditdatamw.zerocell.converter", "Converters"); 44 | 45 | TypeName listOfData = ParameterizedTypeName.get(list, dataClass); 46 | TypeName zeroCellReader = ParameterizedTypeName.get(ClassName.get(ZeroCellReader.class), dataClass); 47 | 48 | MethodSpec reset = MethodSpec.methodBuilder("reset") 49 | .addModifiers(Modifier.PRIVATE) 50 | .addStatement("this.currentRow = -1") 51 | .addStatement("this.currentCol = -1") 52 | .addStatement("this.cur = null") 53 | .addStatement("this.data.clear()") 54 | .build(); 55 | 56 | MethodSpec read = MethodSpec.methodBuilder("read") 57 | .addModifiers(Modifier.PUBLIC) 58 | .addAnnotation(Override.class) 59 | .addParameter(java.io.File.class, "file", Modifier.FINAL) 60 | .addParameter(String.class, "sheet", Modifier.FINAL) 61 | .returns(listOfData) 62 | .addStatement("this.reset()") 63 | .addStatement("$T.process(file, sheet, this)", readerUtil) 64 | .addStatement("List<$T> dataList", typeElement) 65 | .addStatement("dataList = $T.unmodifiableList(this.data)", Collections.class) 66 | .addStatement("return dataList") 67 | .build(); 68 | 69 | MethodSpec.Builder startRowBuilder = MethodSpec.methodBuilder("startRow") 70 | .addAnnotation(Override.class) 71 | .addModifiers(Modifier.PUBLIC) 72 | .addParameter(Integer.TYPE, "i", Modifier.FINAL) 73 | .addStatement("currentRow = i") 74 | .addStatement("isHeaderRow = false") 75 | .addComment("Skip header row") 76 | .beginControlFlow("if (currentRow == 0)") 77 | .addStatement("isHeaderRow=true") 78 | .addStatement("return") 79 | .endControlFlow() 80 | .addStatement("cur = new $T()", dataClass); 81 | 82 | checkRowNumberField().ifPresent(fieldName -> { 83 | //we'll get something like startRowBuilder.addStatement("cur.setRowNumber(currentRow)"); 84 | startRowBuilder.addStatement("cur.set$L(currentRow)", fieldName); 85 | }); 86 | 87 | MethodSpec startRow = startRowBuilder.build(); 88 | 89 | MethodSpec endRow = MethodSpec.methodBuilder("endRow") 90 | .addAnnotation(Override.class) 91 | .addModifiers(Modifier.PUBLIC) 92 | .addParameter(Integer.TYPE, "i", Modifier.FINAL) 93 | .beginControlFlow("if (! java.util.Objects.isNull(cur))") 94 | .addStatement("this.data.add(cur)") 95 | .addStatement("this.cur = null") 96 | .endControlFlow() 97 | .build(); 98 | 99 | MethodSpec headerFooter = MethodSpec.methodBuilder("headerFooter") 100 | .addAnnotation(Override.class) 101 | .addModifiers(Modifier.PUBLIC) 102 | .addParameter(String.class, "text", Modifier.FINAL) 103 | .addParameter(boolean.class, "b", Modifier.FINAL) 104 | .addParameter(String.class, "tagName", Modifier.FINAL) 105 | .addComment("Skip, not processing headers or footers here") 106 | .build(); 107 | 108 | // fields to spec 109 | List columnIndexFields = new ArrayList<>(); 110 | 111 | List columnInfoList = columnsOf(typeElement); 112 | 113 | columnInfoList.forEach(column -> { 114 | int idx= column.getIndex(); 115 | String name = column.getName(); 116 | String normalizedName = String.format("COL_%s", column.getIndex()).toUpperCase(); 117 | columnIndexFields.add(FieldSpec.builder( 118 | Integer.TYPE, 119 | normalizedName, 120 | Modifier.STATIC, Modifier.FINAL) 121 | .initializer("$L", idx) 122 | .build()); 123 | }); 124 | 125 | MethodSpec constructor = MethodSpec.constructorBuilder() 126 | .addModifiers(Modifier.PUBLIC) 127 | .addStatement("this.data = new $T<>()", arrayList) 128 | .build(); 129 | 130 | MethodSpec cell = CellMethodSpec.build(columnInfoList); 131 | 132 | MethodSpec assertColumnName = MethodSpec.methodBuilder("assertColumnName") 133 | .addParameter(String.class, "columnName", Modifier.FINAL) 134 | .addParameter(String.class, "value", Modifier.FINAL) 135 | .beginControlFlow("if (validateHeaders && isHeaderRow)") 136 | .beginControlFlow("if (! columnName.equalsIgnoreCase(value))") 137 | .addStatement("throw new $T(String.format($S, columnName, value))", ZeroCellException.class, "Expected Column '%s' but found '%s'") 138 | .endControlFlow() 139 | .endControlFlow() 140 | .build(); 141 | 142 | TypeSpec readerTypeSpec = TypeSpec.classBuilder(readerClassName) 143 | .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 144 | .addSuperinterface(zeroCellReader) 145 | .addField(FieldSpec.builder(Logger.class, "LOGGER", Modifier.PRIVATE, Modifier.STATIC) 146 | .initializer("$T.getLogger($L.class)", LoggerFactory.class, typeElement.getQualifiedName()) 147 | .build()) 148 | .addFields(columnIndexFields) 149 | .addField(FieldSpec.builder(Boolean.TYPE, "validateHeaders", Modifier.PRIVATE).build()) 150 | .addField(FieldSpec.builder(Boolean.TYPE, "isHeaderRow", Modifier.PRIVATE).build()) 151 | .addField(FieldSpec.builder(Integer.TYPE, "currentRow", Modifier.PRIVATE).build()) 152 | .addField(FieldSpec.builder(Integer.TYPE, "currentCol", Modifier.PRIVATE).build()) 153 | .addField(FieldSpec.builder(dataClass, "cur", Modifier.PRIVATE).build()) 154 | .addField(FieldSpec.builder(listOfData, "data", Modifier.PRIVATE).build()) 155 | .addMethod(read) 156 | .addMethod(reset) 157 | .addMethod(constructor) 158 | .addMethod(headerFooter) 159 | .addMethod(startRow) 160 | .addMethod(cell) 161 | .addMethod(endRow) 162 | .addMethod(assertColumnName) 163 | .build(); 164 | 165 | final JavaFile javaFile = JavaFile.builder(dataClass.packageName(), readerTypeSpec ) 166 | .addStaticImport(convertersClass, "*") 167 | .build(); 168 | LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class) 169 | .info("Generated reader class: {}", readerTypeSpec.name); 170 | return javaFile; 171 | } 172 | 173 | private void assertReaderName() { 174 | if (! Pattern.matches("[A-Za-z]+\\d*[A-Za-z]", readerClassName)) { 175 | throw new IllegalArgumentException("Invalid name for the reader Class: " + readerClassName); 176 | } 177 | } 178 | 179 | private Optional checkRowNumberField() { 180 | for(Element element: typeElement.getEnclosedElements()) { 181 | if (! element.getKind().isField()) { 182 | continue; 183 | } 184 | RowNumber annotation = element.getAnnotation(RowNumber.class); 185 | if (! Objects.isNull(annotation)) { 186 | TypeMirror type; 187 | try { 188 | type = element.asType(); 189 | } catch (MirroredTypeException mte) { 190 | type = mte.getTypeMirror(); 191 | } 192 | final String fieldType = String.format("%s", type); 193 | final String fieldName = element.getSimpleName().toString(); 194 | if (fieldType.equals(int.class.getTypeName()) || 195 | fieldType.equals(long.class.getTypeName()) || 196 | fieldType.equals(Integer.class.getTypeName()) || 197 | fieldType.equals(Long.class.getTypeName()) 198 | ) { 199 | return Optional.of(beanSetterPropertyName(fieldName)); 200 | } else { 201 | // Must be one of the integer classes or bust! 202 | throw new IllegalArgumentException( 203 | String.format("Invalid type (%s) for @RowNumber field (%s). Only java.lang.Integer and java.lang.Long are allowed", fieldType, fieldName)); 204 | } 205 | } 206 | } 207 | return Optional.empty(); 208 | } 209 | } 210 | --------------------------------------------------------------------------------