├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── deploy.cmd ├── install.cmd ├── pom.xml ├── src ├── main │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── nambach │ │ │ └── excelutil │ │ │ ├── constraint │ │ │ ├── Constraint.java │ │ │ ├── ConstraintHandler.java │ │ │ └── ConstraintProperty.java │ │ │ ├── core │ │ │ ├── BaseEditor.java │ │ │ ├── BaseReader.java │ │ │ ├── BaseWriter.java │ │ │ ├── ColumnMapper.java │ │ │ ├── ColumnTemplate.java │ │ │ ├── DataTemplate.java │ │ │ ├── Editor.java │ │ │ ├── FlatData.java │ │ │ ├── FreestyleWriter.java │ │ │ ├── Handler.java │ │ │ ├── HandlerMap.java │ │ │ ├── MergeItem.java │ │ │ ├── Navigation.java │ │ │ ├── Pointer.java │ │ │ ├── PointerNavigation.java │ │ │ ├── Raw.java │ │ │ ├── ReaderCell.java │ │ │ ├── ReaderConfig.java │ │ │ ├── ReaderController.java │ │ │ ├── ReaderRow.java │ │ │ ├── Result.java │ │ │ ├── RowError.java │ │ │ ├── SequentialWriter.java │ │ │ ├── Template.java │ │ │ ├── WriterCell.java │ │ │ └── WriterComment.java │ │ │ ├── style │ │ │ ├── Border.java │ │ │ ├── BorderSide.java │ │ │ ├── CacheStyle.java │ │ │ ├── HSSFColorCache.java │ │ │ ├── HSSFStyleHandler.java │ │ │ ├── Node.java │ │ │ ├── Style.java │ │ │ ├── StyleColor.java │ │ │ ├── StyleConstant.java │ │ │ ├── StyleHandler.java │ │ │ ├── StyleProperty.java │ │ │ ├── XSSFColorCache.java │ │ │ └── XSSFStyleHandler.java │ │ │ ├── util │ │ │ ├── Comparing.java │ │ │ ├── Copyable.java │ │ │ ├── CopyableList.java │ │ │ ├── CopyableMap.java │ │ │ ├── Criterion.java │ │ │ ├── Direction.java │ │ │ ├── FileUtil.java │ │ │ ├── ListUtil.java │ │ │ ├── NullPolicy.java │ │ │ ├── PixelUtil.java │ │ │ ├── Readable.java │ │ │ ├── ReflectUtil.java │ │ │ └── TextUtil.java │ │ │ └── validator │ │ │ ├── Constraint.java │ │ │ ├── Field.java │ │ │ ├── FieldError.java │ │ │ ├── ObjectError.java │ │ │ ├── Validator.java │ │ │ └── builtin │ │ │ ├── DecimalConstraint.java │ │ │ ├── DecimalValidator.java │ │ │ ├── IntegerConstraint.java │ │ │ ├── IntegerValidator.java │ │ │ ├── StringConstraint.java │ │ │ ├── StringValidator.java │ │ │ ├── TypeValidator.java │ │ │ └── Util.java │ └── resources │ │ ├── img │ │ ├── ex01.jpg │ │ └── ex02.jpg │ │ └── log4j2.xml └── test │ └── java │ ├── model │ ├── Book.java │ ├── Constant.java │ └── LargeConstant.java │ ├── multithread │ ├── Data.java │ ├── TestRead.java │ └── TestWrite.java │ ├── navigation │ └── NavigationSheet.java │ ├── read │ ├── BookValidator.java │ ├── Sample1.java │ ├── Sample2.java │ ├── Sample3.java │ ├── TestFromColumn.java │ ├── TestRawRead.java │ └── TestReadConfig.java │ ├── resources │ ├── fiction.xlsx │ ├── nonFiction.xlsx │ ├── science.xlsx │ ├── test-books-no-header.xlsx │ └── test-books.xlsx │ ├── style │ └── TestStyle.java │ ├── validator │ ├── NumericValidator.java │ └── User.java │ └── write │ ├── LargeMerge.java │ ├── LargeSample.java │ ├── Sample1.java │ ├── Sample2.java │ ├── Sample3.java │ ├── Sample4.java │ └── Sample5.java ├── version.cmd └── wiki ├── cheatsheet.md ├── custom-template.md ├── excel-97.md ├── img ├── benchmark_writing.png ├── comment.png ├── custom-styles.png ├── custom-template.png ├── dropdown.png ├── excel97compare.png ├── expand-rows.png ├── merge-rows.png ├── reader-validation-1.png ├── style-priority-example.png ├── style-priority.png └── style-tree.png ├── others.md ├── read-dto.md ├── reading-validation.md ├── res └── image.pptx ├── style.md ├── use-editor.md └── write-dto-objects.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Code Snippet** 14 | The snippet of code that conducts the bug. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 22 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | ### Current project ### 34 | src/main/resources/*.xlsx -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | nambach2@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set /P confirm=Are you sure? : 3 | if "%confirm%"=="y" ( 4 | echo.Processing... 5 | call mvn clean deploy 6 | ) 7 | -------------------------------------------------------------------------------- /install.cmd: -------------------------------------------------------------------------------- 1 | :: https://stackoverflow.com/a/4955695 2 | 3 | mvn install:install-file ^ 4 | -Dfile=target/ExcelUtil-1.0.jar ^ 5 | -DgroupId=io.nambm ^ 6 | -DartifactId=ExcelUtil ^ 7 | -Dversion=1.0 ^ 8 | -Dpackaging=jar ^ 9 | -DgeneratePom=true -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/constraint/Constraint.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.constraint; 2 | 3 | import io.github.nambach.excelutil.util.Copyable; 4 | import io.github.nambach.excelutil.util.CopyableList; 5 | import io.github.nambach.excelutil.util.Readable; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.UUID; 14 | 15 | import static io.github.nambach.excelutil.constraint.ConstraintProperty.Dropdown; 16 | 17 | public class Constraint implements Copyable { 18 | 19 | @Getter(AccessLevel.PUBLIC) 20 | private final String uuid; 21 | private final Map map = new HashMap<>(); 22 | 23 | private Constraint(String uuid) { 24 | this.uuid = uuid; 25 | } 26 | 27 | private static Constraint newRandom() { 28 | return new Constraint(UUID.randomUUID().toString()); 29 | } 30 | 31 | public static ConstraintBuilder builder() { 32 | return new ConstraintBuilder(newRandom()); 33 | } 34 | 35 | public static ConstraintBuilder builder(Constraint constraint) { 36 | if (constraint != null) { 37 | return new ConstraintBuilder(constraint.makeCopy()); 38 | } else { 39 | return builder(); 40 | } 41 | } 42 | 43 | void put(ConstraintProperty property) { 44 | map.put(property.getName(), property); 45 | } 46 | 47 | ConstraintProperty getProperty(ConstraintProperty any) { 48 | return map.getOrDefault(any.getName(), any); 49 | } 50 | 51 | ConstraintProperty getOrDefault(ConstraintProperty any) { 52 | ConstraintProperty property = map.get(any.getName()); 53 | 54 | if (property == null) { 55 | put(any); 56 | property = any; 57 | } 58 | 59 | return property; 60 | } 61 | 62 | boolean hasNoProperty() { 63 | return map.isEmpty() || map.values().stream().allMatch(Readable::isNullOrEmpty); 64 | } 65 | 66 | /** 67 | * @return a shallow copied of this constraint 68 | */ 69 | @Override 70 | public Constraint makeCopy() { 71 | Constraint copy = newRandom(); 72 | this.map.forEach((name, property) -> { 73 | copy.map.put(name, property.makeCopy()); 74 | }); 75 | return copy; 76 | } 77 | 78 | Constraint accumulate(Constraint other) { 79 | if (this == other || other == null || other.hasNoProperty()) { 80 | return this; 81 | } 82 | 83 | Constraint accumulated = this.makeCopy(); 84 | other.map.values().stream() 85 | .filter(Readable::hasValue) 86 | .forEach(accumulated::put); 87 | return accumulated; 88 | } 89 | 90 | public static class ConstraintBuilder { 91 | private final Constraint constraint; 92 | 93 | private ConstraintBuilder(Constraint constraint) { 94 | this.constraint = constraint; 95 | } 96 | 97 | @SuppressWarnings({"unchecked"}) 98 | public ConstraintBuilder dropdown(String... values) { 99 | if (values != null) { 100 | constraint.getOrDefault(Dropdown.withValue(new CopyableList())) 101 | .getAny(CopyableList.class) 102 | .ifPresent(l -> Collections.addAll(l, values)); 103 | } 104 | return this; 105 | } 106 | 107 | @SuppressWarnings({"unchecked"}) 108 | public ConstraintBuilder dropdown(Collection values) { 109 | constraint.getOrDefault(Dropdown.withValue(new CopyableList())) 110 | .getAny(CopyableList.class) 111 | .ifPresent(l -> l.addAll(values)); 112 | return this; 113 | } 114 | 115 | public Constraint build() { 116 | return constraint; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/constraint/ConstraintHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.constraint; 2 | 3 | import org.apache.poi.ss.usermodel.Cell; 4 | import org.apache.poi.ss.usermodel.DataValidation; 5 | import org.apache.poi.ss.usermodel.DataValidationConstraint; 6 | import org.apache.poi.ss.usermodel.DataValidationHelper; 7 | import org.apache.poi.ss.usermodel.Sheet; 8 | import org.apache.poi.ss.usermodel.Workbook; 9 | import org.apache.poi.ss.util.CellRangeAddressList; 10 | import org.apache.poi.xssf.usermodel.XSSFDataValidation; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static io.github.nambach.excelutil.constraint.ConstraintProperty.Dropdown; 18 | 19 | public class ConstraintHandler { 20 | private final Workbook workbook; 21 | private final Map cache = new HashMap<>(); 22 | 23 | public ConstraintHandler(Workbook workbook) { 24 | this.workbook = workbook; 25 | } 26 | 27 | private String computeKey(Constraint constraint, Sheet sheet) { 28 | return String.format("%s:%s", workbook.getSheetIndex(sheet), constraint.getUuid()); 29 | } 30 | 31 | private DataValidation getCache(Constraint constraint, Sheet sheet) { 32 | return cache.get(computeKey(constraint, sheet)); 33 | } 34 | 35 | private void putCache(Constraint constraint, Sheet sheet, DataValidation validation) { 36 | cache.put(computeKey(constraint, sheet), validation); 37 | } 38 | 39 | public void applyConstraint(Constraint constraint, Cell cell) { 40 | if (constraint == null) { 41 | return; 42 | } 43 | 44 | Sheet sheet = cell.getSheet(); 45 | 46 | DataValidation validation = getCache(constraint, sheet); 47 | if (validation != null) { 48 | validation.getRegions().addCellRangeAddress(cell.getRowIndex(), cell.getColumnIndex(), 49 | cell.getRowIndex(), cell.getColumnIndex()); 50 | return; 51 | } 52 | 53 | 54 | constraint.getProperty(Dropdown).getAny(ArrayList.class).ifPresent(values -> applyDropdown(values, constraint, cell)); 55 | 56 | } 57 | 58 | private void applyDropdown(Collection values, Constraint constraint, Cell cell) { 59 | Sheet sheet = cell.getSheet(); 60 | DataValidationHelper helper = sheet.getDataValidationHelper(); 61 | 62 | String[] strings = values.stream().map(Object::toString).toArray(String[]::new); 63 | DataValidationConstraint dvConstraint = helper.createExplicitListConstraint(strings); 64 | 65 | CellRangeAddressList addressList = new CellRangeAddressList(cell.getRowIndex(), cell.getRowIndex(), 66 | cell.getColumnIndex(), cell.getColumnIndex()); 67 | DataValidation validation = helper.createValidation(dvConstraint, addressList); 68 | // Note the check on the actual type of the DataValidation object. 69 | // If it is an instance of the XSSFDataValidation class then the 70 | // boolean value 'false' must be passed to the setSuppressDropDownArrow() 71 | // method and an explicit call made to the setShowErrorBox() method. 72 | if (validation instanceof XSSFDataValidation) { 73 | validation.setSuppressDropDownArrow(true); 74 | validation.setShowErrorBox(true); 75 | } else { 76 | // If the DataValidation contains an instance of the HSSFDataValidation 77 | // class then 'true' should be passed to the setSuppressDropDownArrow() 78 | // method and the call to setShowErrorBox() is not necessary. 79 | validation.setSuppressDropDownArrow(false); 80 | } 81 | sheet.addValidationData(validation); 82 | 83 | // save cache 84 | putCache(constraint, sheet, validation); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/constraint/ConstraintProperty.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.constraint; 2 | 3 | import io.github.nambach.excelutil.util.Copyable; 4 | import io.github.nambach.excelutil.util.Readable; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.With; 8 | 9 | @Getter 10 | @AllArgsConstructor 11 | class ConstraintProperty implements Readable, Copyable { 12 | static final ConstraintProperty Dropdown = new ConstraintProperty("dropdown", null); 13 | 14 | private final String name; 15 | @With 16 | private final Object value; 17 | 18 | @Override 19 | public ConstraintProperty makeCopy() { 20 | return new ConstraintProperty(name, this.copyValue()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/BaseEditor.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import org.apache.poi.ss.usermodel.Cell; 4 | import org.apache.poi.ss.usermodel.Row; 5 | import org.apache.poi.ss.usermodel.Sheet; 6 | 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.util.Date; 10 | 11 | interface BaseEditor { 12 | default Row getRowAt(Sheet sheet, int rowAt) { 13 | Row row = sheet.getRow(rowAt); 14 | if (row == null) { 15 | row = sheet.createRow(rowAt); 16 | } 17 | return row; 18 | } 19 | 20 | default Cell getCellAt(Row row, int colAt) { 21 | return row.getCell(colAt, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); 22 | } 23 | 24 | default Cell getCellAt(Sheet sheet, PointerNavigation navigation) { 25 | Row row = getRowAt(sheet, navigation.getRow()); 26 | return getCellAt(row, navigation.getCol()); 27 | } 28 | 29 | default ReaderCell getReaderCellAt(Sheet sheet, PointerNavigation pointer) { 30 | Cell cell = getCellAt(getRowAt(sheet, pointer.getRow()), pointer.getCol()); 31 | return ReaderCell.wrap(cell); 32 | } 33 | 34 | default boolean isDateType(Object value) { 35 | return value instanceof Date || 36 | value instanceof LocalDateTime || 37 | value instanceof LocalDate; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/FlatData.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.ArrayList; 7 | 8 | @Getter 9 | @Setter 10 | class FlatData extends ArrayList { 11 | 12 | FlatData() { 13 | } 14 | 15 | public FlatData makeCopy() { 16 | FlatData clone = new FlatData(); 17 | clone.addAll(this); 18 | return clone; 19 | } 20 | 21 | public Object getLast() { 22 | if (isEmpty()) return null; 23 | return get(size() - 1); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/FreestyleWriter.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import io.github.nambach.excelutil.constraint.Constraint; 4 | import io.github.nambach.excelutil.style.Style; 5 | 6 | import java.util.Collection; 7 | import java.util.Date; 8 | import java.util.function.UnaryOperator; 9 | 10 | interface FreestyleWriter> extends Navigation { 11 | T useStyle(Style style); 12 | 13 | T applyStyle(); 14 | 15 | T applyStyle(Style style, String... address); 16 | 17 | T applyStyle(Style style, Collection addresses); 18 | 19 | T applyConstraint(Constraint constraint, String... address); 20 | 21 | T applyConstraint(Constraint constraint, Collection addresses); 22 | 23 | T writeComment(UnaryOperator builder); 24 | 25 | default T comment(String comment) { 26 | return writeComment(c -> c.content(comment)); 27 | } 28 | 29 | T writeCell(UnaryOperator builder); 30 | 31 | default T date(Date date) { 32 | return writeCell(c -> c.date(date)); 33 | } 34 | 35 | default T number(double number) { 36 | return writeCell(c -> c.number(number)); 37 | } 38 | 39 | default T text(String text) { 40 | return writeCell(c -> c.text(text)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/Handler.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import io.github.nambach.excelutil.util.ReflectUtil; 4 | import io.github.nambach.excelutil.validator.builtin.TypeValidator; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.extern.log4j.Log4j2; 9 | import lombok.var; 10 | 11 | import java.beans.PropertyDescriptor; 12 | import java.lang.reflect.Method; 13 | import java.time.LocalDateTime; 14 | import java.util.Date; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.Objects; 18 | import java.util.function.BiConsumer; 19 | import java.util.function.Function; 20 | import java.util.function.UnaryOperator; 21 | 22 | /** 23 | * An entity that specifies way to read data from Excel and store into DTO 24 | * 25 | * @param DTO 26 | */ 27 | @Getter(AccessLevel.PACKAGE) 28 | @Setter(AccessLevel.PACKAGE) 29 | @Log4j2 30 | public class Handler { 31 | 32 | private static final Map, Function> FIELD_READERS = new HashMap<>(); 33 | 34 | static { 35 | var o = FIELD_READERS; 36 | o.put(String.class, ReaderCell::readString); 37 | o.put(Long.class, ReaderCell::readLong); 38 | o.put(long.class, ReaderCell::readLong); 39 | o.put(Integer.class, ReaderCell::readInt); 40 | o.put(int.class, ReaderCell::readInt); 41 | o.put(Double.class, ReaderCell::readDouble); 42 | o.put(double.class, ReaderCell::readDouble); 43 | o.put(Float.class, ReaderCell::readFloat); 44 | o.put(float.class, ReaderCell::readFloat); 45 | o.put(Boolean.class, ReaderCell::readBoolean); 46 | o.put(boolean.class, ReaderCell::readBoolean); 47 | o.put(Date.class, ReaderCell::readDate); 48 | o.put(LocalDateTime.class, ReaderCell::readLocalDateTime); 49 | } 50 | 51 | private Integer colAt; 52 | private Integer colFrom; 53 | private String colTitle; 54 | private String fieldName; 55 | private BiConsumer coreHandler; 56 | private TypeValidator typeValidator; 57 | 58 | Handler() { 59 | } 60 | 61 | /** 62 | * Specify the column to handle. 63 | * 64 | * @param i column index (from 0) 65 | * @return current handler 66 | */ 67 | public Handler atColumn(int i) { 68 | colAt = i; 69 | return this; 70 | } 71 | 72 | public Handler atColumn(String colTitle) { 73 | this.colTitle = colTitle; 74 | return this; 75 | } 76 | 77 | /** 78 | * Specify the beginning column to start handle. 79 | * 80 | * @param i start index (from 0) 81 | * @return current handler 82 | */ 83 | public Handler fromColumn(int i) { 84 | colFrom = i; 85 | return this; 86 | } 87 | 88 | /** 89 | * Specify the property of DTO to store the cell value into. 90 | * 91 | * @param s DTO field name 92 | * @return current handler 93 | */ 94 | Handler field(String s) { 95 | fieldName = s; 96 | return this; 97 | } 98 | 99 | public Handler validate(TypeValidator typeValidator) { 100 | this.typeValidator = typeValidator; 101 | return this; 102 | } 103 | 104 | public Handler validate(UnaryOperator builder) { 105 | this.typeValidator = builder.apply(TypeValidator.init()); 106 | return this; 107 | } 108 | 109 | /** 110 | * Set a custom {@link BiConsumer} to handle storing cell value into DTO. 111 | * 112 | * @param handler a {@link BiConsumer} that has 2 parameter, the DTO and the {@link ReaderCell} 113 | * @return current handler 114 | */ 115 | public Handler handle(BiConsumer handler) { 116 | Objects.requireNonNull(handler); 117 | this.coreHandler = ReflectUtil.safeWrap(handler); 118 | return this; 119 | } 120 | 121 | boolean needValidation() { 122 | return typeValidator != null; 123 | } 124 | 125 | protected Handler wrapHandleField(PropertyDescriptor pd) { 126 | Method setter = pd.getWriteMethod(); 127 | Class type = pd.getPropertyType(); 128 | 129 | this.coreHandler = (T object, ReaderCell cell) -> { 130 | Object cellValue; 131 | Function reader = FIELD_READERS.get(type); 132 | if (reader == null) { 133 | cellValue = null; 134 | } else { 135 | cellValue = reader.apply(cell); 136 | } 137 | 138 | try { 139 | setter.invoke(object, cellValue); 140 | } catch (Exception e) { 141 | log.error("Error while invoking setter.", e); 142 | } 143 | }; 144 | 145 | return this; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/HandlerMap.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | import java.util.stream.Stream; 8 | 9 | class HandlerMap { 10 | private final Handlers empty = new Handlers<>(); 11 | 12 | private final Map> indexAtMap = new HashMap<>(); 13 | private final Map> indexFromMap = new HashMap<>(); 14 | private final Map> titleMap = new HashMap<>(); 15 | 16 | private static void shiftIndexMap(int indexOffset, Map> target) { 17 | Map> temp = new HashMap<>(); 18 | target.forEach((i, handler) -> temp.put(i + indexOffset, handler)); 19 | target.clear(); 20 | target.putAll(temp); 21 | } 22 | 23 | private Handlers getIndexFrom(int colIndex) { 24 | Handlers rs = new Handlers<>(); 25 | for (int i = 0; i <= colIndex; i++) { 26 | rs.concat(indexFromMap.getOrDefault(i, empty)); 27 | } 28 | return rs; 29 | } 30 | 31 | public Handlers get(int colIndex, String colTitle) { 32 | Handlers indexHandlers = indexAtMap.getOrDefault(colIndex, empty); 33 | Handlers indexFromHandlers = getIndexFrom(colIndex); 34 | Handlers titleHandlers = titleMap.getOrDefault(colTitle, empty); 35 | 36 | return new Handlers() 37 | .concat(indexHandlers) 38 | .concat(indexFromHandlers) 39 | .concat(titleHandlers); 40 | } 41 | 42 | public void putAt(int index, Handler handler) { 43 | indexAtMap.putIfAbsent(index, new Handlers<>()); 44 | indexAtMap.get(index).add(handler); 45 | } 46 | 47 | public void putFrom(int index, Handler handler) { 48 | indexFromMap.putIfAbsent(index, new Handlers<>()); 49 | indexFromMap.get(index).add(handler); 50 | } 51 | 52 | public void put(String title, Handler handler) { 53 | titleMap.putIfAbsent(title, new Handlers<>()); 54 | titleMap.get(title).add(handler); 55 | } 56 | 57 | public HandlerMap makeCopy() { 58 | HandlerMap clone = new HandlerMap<>(); 59 | clone.indexAtMap.putAll(this.indexAtMap); 60 | clone.indexFromMap.putAll(this.indexFromMap); 61 | clone.titleMap.putAll(this.titleMap); 62 | return clone; 63 | } 64 | 65 | public void shiftIndexMap(int indexOffset) { 66 | shiftIndexMap(indexOffset, indexAtMap); 67 | shiftIndexMap(indexOffset, indexFromMap); 68 | } 69 | 70 | public int getMinIndex() { 71 | Integer min1 = indexAtMap.keySet().stream().reduce(Math::min).orElse(null); 72 | Integer min2 = indexFromMap.keySet().stream().reduce(Math::min).orElse(null); 73 | return Stream.of(min1, min2) 74 | .filter(Objects::nonNull) 75 | .reduce(Math::min).orElse(0); 76 | } 77 | 78 | static class Handlers extends ArrayList> { 79 | 80 | public Handlers concat(Handlers c) { 81 | super.addAll(c); 82 | return this; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/MergeItem.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.apache.poi.ss.usermodel.Cell; 6 | import org.apache.poi.ss.util.CellRangeAddress; 7 | 8 | @Getter 9 | @Setter 10 | class MergeItem { 11 | private Object lastValue; 12 | private int fromRow; 13 | private int toRow; 14 | 15 | public MergeItem(Object lastValue, int fromRow, int toRow) { 16 | this.lastValue = lastValue; 17 | this.fromRow = fromRow; 18 | this.toRow = toRow; 19 | } 20 | 21 | public void handleMerge(Cell cell) { 22 | if (this.needMerge()) { 23 | cell.getCellStyle().setWrapText(true); 24 | cell.getSheet() 25 | .addMergedRegionUnsafe(new CellRangeAddress(this.fromRow, this.toRow, 26 | cell.getColumnIndex(), cell.getColumnIndex())); 27 | } 28 | } 29 | 30 | public void reset(Object currentValue, int startRow) { 31 | fromRow = startRow; 32 | toRow = startRow; 33 | lastValue = currentValue; 34 | } 35 | 36 | public void increaseRange() { 37 | toRow++; 38 | } 39 | 40 | public boolean needMerge() { 41 | return toRow - fromRow > 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/Navigation.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import org.apache.poi.ss.util.CellAddress; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.TreeSet; 8 | 9 | interface Navigation> { 10 | 11 | default Collection parseAddress(Collection addresses) { 12 | if (addresses == null || addresses.isEmpty()) { 13 | return Collections.emptyList(); 14 | } 15 | TreeSet set = new TreeSet<>(CellAddress::compareTo); 16 | for (String address : addresses) { 17 | if (address.contains(":")) { 18 | String[] split = address.split(":"); 19 | CellAddress from = new CellAddress(split[0].trim()); 20 | CellAddress to = new CellAddress(split[1].trim()); 21 | for (int rowNo = from.getRow(); rowNo <= to.getRow(); rowNo++) { 22 | for (int colNo = from.getColumn(); colNo <= to.getColumn(); colNo++) { 23 | set.add(new CellAddress(rowNo, colNo)); 24 | } 25 | } 26 | } else { 27 | set.add(new CellAddress(address.trim())); 28 | } 29 | } 30 | return set; 31 | } 32 | 33 | T goToCell(String address); 34 | 35 | T goToCell(int row, int col); 36 | 37 | T next(); 38 | 39 | T next(int steps); 40 | 41 | T down(); 42 | 43 | T down(int steps); 44 | 45 | T enter(); 46 | 47 | T enter(int steps); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/Pointer.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.apache.poi.ss.util.CellAddress; 7 | 8 | @Setter(AccessLevel.NONE) 9 | @Getter 10 | class Pointer { 11 | private int row; 12 | private int col; 13 | 14 | Pointer() { 15 | } 16 | 17 | public Pointer(int row, int col) { 18 | this.row = row; 19 | this.col = col; 20 | } 21 | 22 | public void sync(Pointer p) { 23 | this.row = p.row; 24 | this.col = p.col; 25 | } 26 | 27 | public void reset() { 28 | this.row = 0; 29 | this.col = 0; 30 | } 31 | 32 | public void update(String address) { 33 | CellAddress a = new CellAddress(address); 34 | update(a.getRow(), a.getColumn()); 35 | } 36 | 37 | public void update(int row, int col) { 38 | if (row < 0 || col < 0) { 39 | throw new RuntimeException("Coordinate must be from 0."); 40 | } 41 | this.row = row; 42 | this.col = col; 43 | } 44 | 45 | public void moveRight() { 46 | this.col++; 47 | } 48 | 49 | public void moveRight(int steps) { 50 | this.col += steps; 51 | } 52 | 53 | public void jumpRight(Pointer other) { 54 | int colGap = other.col - this.col; 55 | if (colGap < 0) { 56 | colGap = 0; 57 | } 58 | this.col += 1 + colGap; 59 | } 60 | 61 | public void moveDown() { 62 | this.row++; 63 | } 64 | 65 | public void moveDown(int steps) { 66 | this.row += steps; 67 | } 68 | 69 | public void jumpDown(Pointer other) { 70 | int rowGap = other.row - this.row; 71 | if (rowGap < 0) { 72 | rowGap = 0; 73 | } 74 | this.row += 1 + rowGap; 75 | } 76 | 77 | public void enter() { 78 | this.row++; 79 | this.col = 0; 80 | } 81 | 82 | public void enter(Pointer other) { 83 | jumpDown(other); 84 | this.col = 0; 85 | } 86 | 87 | public boolean same(Pointer other) { 88 | return this.row == other.row && this.col == other.col; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/PointerNavigation.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import org.apache.poi.ss.util.CellAddress; 4 | 5 | class PointerNavigation { 6 | private final Pointer pointer = new Pointer(); 7 | private final Pointer pivot = new Pointer(); 8 | 9 | public CellAddress getCellAddress() { 10 | return new CellAddress(pointer.getRow(), pointer.getCol()); 11 | } 12 | 13 | public int getRow() { 14 | return pointer.getRow(); 15 | } 16 | 17 | public int getCol() { 18 | return pointer.getCol(); 19 | } 20 | 21 | public void goToCell(String address) { 22 | pointer.update(address); 23 | pivot.sync(pointer); 24 | } 25 | 26 | public void goToCell(int row, int col) { 27 | pointer.update(row, col); 28 | pivot.sync(pointer); 29 | } 30 | 31 | public void next() { 32 | pointer.jumpRight(pivot); 33 | pivot.sync(pointer); 34 | } 35 | 36 | public void next(int steps) { 37 | if (steps > 0) { 38 | next(); 39 | pointer.moveRight(steps - 1); 40 | pivot.sync(pointer); 41 | } 42 | } 43 | 44 | public void down() { 45 | pointer.jumpDown(pivot); 46 | pivot.sync(pointer); 47 | } 48 | 49 | public void down(int steps) { 50 | if (steps > 0) { 51 | down(); 52 | pointer.moveDown(steps - 1); 53 | pivot.sync(pointer); 54 | } 55 | } 56 | 57 | public void enter() { 58 | // pointer.update(getNextRowIndex(), 0); 59 | pointer.enter(); 60 | pivot.sync(pointer); 61 | } 62 | 63 | public void enter(int steps) { 64 | if (steps > 0) { 65 | enter(); 66 | pointer.moveDown(steps - 1); 67 | pivot.sync(pointer); 68 | } 69 | } 70 | 71 | public void update(int row, int col) { 72 | pointer.update(row, col); 73 | pivot.sync(pointer); 74 | } 75 | 76 | public void updatePivot(int row, int col) { 77 | pivot.update(row, col); 78 | } 79 | 80 | public void updatePivotRight(int steps) { 81 | pivot.moveRight(steps); 82 | } 83 | 84 | public void updatePivotDown(int steps) { 85 | pivot.moveDown(steps); 86 | } 87 | 88 | public void sync(PointerNavigation other) { 89 | this.pointer.sync(other.pointer); 90 | this.pivot.sync(other.pivot); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/Raw.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * A wrapper of DTO that contains the resulted DTO after reading process 13 | * and other properties found while reading the Excel file. 14 | * 15 | * @param DTO 16 | */ 17 | @Getter 18 | @Setter(AccessLevel.PACKAGE) 19 | @ToString 20 | public class Raw { 21 | /** 22 | * A map that contains other values found while reading a DTO row. 23 | * These values might have no mapping rules so that it cannot be 24 | * mapped into the DTO class. 25 | *

26 | * Map Key: column title or column index 27 | * Map Value: cell value of DTO row 28 | */ 29 | private final Map otherData; 30 | /** 31 | * DTO resulted from the reading process. 32 | */ 33 | private T data; 34 | 35 | Raw() { 36 | otherData = new LinkedHashMap<>(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/ReaderController.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | 8 | public abstract class ReaderController { 9 | protected Result result; 10 | 11 | @Getter(AccessLevel.PACKAGE) 12 | private boolean isExitNow; 13 | 14 | @Getter(AccessLevel.PACKAGE) 15 | private boolean isEarlyExit; 16 | 17 | ReaderController(ReaderConfig config, Result result) { 18 | if (config != null) { 19 | this.isEarlyExit = config.isEarlyExit(); 20 | } 21 | this.result = result; 22 | } 23 | 24 | public boolean hasError() { 25 | return result.hasErrors(); 26 | } 27 | 28 | public List getErrors() { 29 | return result.getErrors(); 30 | } 31 | 32 | public abstract void setError(String message); 33 | 34 | public abstract void throwError(String message); 35 | 36 | public void terminateNow() { 37 | this.isExitNow = true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/ReaderRow.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import org.apache.poi.ss.usermodel.Row; 6 | 7 | public class ReaderRow extends ReaderController { 8 | private final Row row; 9 | 10 | @Getter(AccessLevel.PACKAGE) 11 | private boolean skipThisObject; 12 | 13 | ReaderRow(Row row, ReaderConfig config, Result result) { 14 | super(config, result); 15 | this.row = row; 16 | } 17 | 18 | public void skipThisObject() { 19 | this.skipThisObject = true; 20 | } 21 | 22 | @Override 23 | public void setError(String message) { 24 | result.newRowError(row.getRowNum()).setCustomError(message); 25 | } 26 | 27 | @Override 28 | public void throwError(String message) { 29 | this.setError(message); 30 | super.terminateNow(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/Result.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import io.github.nambach.excelutil.util.ListUtil; 4 | import lombok.AccessLevel; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static io.github.nambach.excelutil.util.ListUtil.findElse; 13 | 14 | @Getter 15 | @EqualsAndHashCode(callSuper = true) 16 | public class Result extends ArrayList implements List { 17 | @Getter(AccessLevel.NONE) 18 | private final Class tClass; 19 | 20 | private final List> rawData = new ArrayList<>(); 21 | private final List errors = new ArrayList<>(); 22 | 23 | public Result(Class tClass) { 24 | this.tClass = tClass; 25 | } 26 | 27 | public boolean hasErrors() { 28 | return ListUtil.hasMember(errors); 29 | } 30 | 31 | public boolean noError() { 32 | return ListUtil.isNullOrEmpty(errors); 33 | } 34 | 35 | void addRaw(Raw raw) { 36 | rawData.add(raw); 37 | this.add(raw.getData()); 38 | } 39 | 40 | RowError newRowError(int index) { 41 | return findElse(errors, 42 | l -> l.getIndex() == index, 43 | new RowError(index, tClass)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/RowError.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import io.github.nambach.excelutil.validator.ObjectError; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.util.List; 8 | 9 | @Getter 10 | public class RowError { 11 | 12 | private final int index; 13 | @Setter 14 | private String customError; 15 | @Setter 16 | private ObjectError objectError; 17 | 18 | public RowError(int index, Class clazz) { 19 | this.index = index; 20 | if (clazz != null) { 21 | objectError = new ObjectError(clazz); 22 | } 23 | } 24 | 25 | void appendError(String field, List messages) { 26 | objectError.appendError(field, messages); 27 | } 28 | 29 | public int getExcelIndex() { 30 | return index + 1; 31 | } 32 | 33 | public String getRowString() { 34 | return "Row " + (index + 1); 35 | } 36 | 37 | public String getMessage() { 38 | if (customError != null) { 39 | return customError; 40 | } 41 | if (objectError != null) { 42 | return objectError.getMessage(); 43 | } 44 | return ""; 45 | } 46 | 47 | public String getInlineMessage() { 48 | if (customError != null) { 49 | return customError; 50 | } 51 | if (objectError != null) { 52 | return objectError.getInlineMessage(); 53 | } 54 | return ""; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return getRowString() + ": " + getInlineMessage(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/SequentialWriter.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.util.Collection; 6 | import java.util.function.UnaryOperator; 7 | 8 | public class SequentialWriter implements AutoCloseable { 9 | private final Editor editor; 10 | 11 | public SequentialWriter() { 12 | editor = new Editor(); 13 | } 14 | 15 | public SequentialWriter(InputStream stream) { 16 | editor = new Editor(stream); 17 | } 18 | 19 | public void createNewSheet(String sheetName) { 20 | editor.goToSheet(sheetName); 21 | } 22 | 23 | public void writeData(DataTemplate template, Collection data) { 24 | editor.writeData(template, data) 25 | .enter(); 26 | } 27 | 28 | public void writeTemplate(Template template) { 29 | editor.writeTemplate(template) 30 | .enter(); 31 | } 32 | 33 | public void writeLine(int indent, UnaryOperator detail) { 34 | editor.next(indent) 35 | .writeCell(detail) 36 | .enter(); 37 | } 38 | 39 | public void skipLines(int lines) { 40 | editor.enter(lines); 41 | } 42 | 43 | public void freeze(int rows, int cols) { 44 | editor.configSheet(cf -> cf.freeze(rows, cols)); 45 | } 46 | 47 | public ByteArrayInputStream exportToFile() { 48 | return editor.exportToFile(); 49 | } 50 | 51 | @Override 52 | public void close() { 53 | this.editor.close(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/WriterCell.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import io.github.nambach.excelutil.constraint.Constraint; 4 | import io.github.nambach.excelutil.style.Style; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.apache.poi.ss.util.CellAddress; 9 | 10 | import java.util.Date; 11 | import java.util.function.UnaryOperator; 12 | 13 | @Getter(AccessLevel.PACKAGE) 14 | @Setter(AccessLevel.PACKAGE) 15 | public class WriterCell { 16 | private String content; 17 | private Double value; 18 | private Date date; 19 | private int rowAt; 20 | private int colAt; 21 | private int rowSpan = 1; 22 | private int colSpan = 1; 23 | private Style style; 24 | private Constraint constraint; 25 | private WriterComment comment; 26 | 27 | WriterCell(CellAddress address, Style style) { 28 | this.rowAt = address.getRow(); 29 | this.colAt = address.getColumn(); 30 | this.style = style; 31 | } 32 | 33 | public WriterCell text(String s) { 34 | this.content = s; 35 | return this; 36 | } 37 | 38 | public WriterCell number(double v) { 39 | this.value = v; 40 | return this; 41 | } 42 | 43 | public WriterCell date(Date d) { 44 | this.date = d; 45 | return this; 46 | } 47 | 48 | public WriterCell date(Date d, String datePattern) { 49 | this.date = d; 50 | if (datePattern != null) { 51 | this.style(s -> s.datePattern(datePattern)); 52 | } 53 | return this; 54 | } 55 | 56 | public WriterCell colSpan(int v) { 57 | if (v > 1) { 58 | this.colSpan = v; 59 | } 60 | return this; 61 | } 62 | 63 | public WriterCell rowSpan(int v) { 64 | if (v > 1) { 65 | this.rowSpan = v; 66 | } 67 | return this; 68 | } 69 | 70 | public WriterCell comment(String comment) { 71 | return this.comment(c -> c.content(comment)); 72 | } 73 | 74 | public WriterCell comment(String comment, String author) { 75 | return this.comment(c -> c.content(comment).author(author)); 76 | } 77 | 78 | public WriterCell comment(UnaryOperator builder) { 79 | this.comment = builder.apply(new WriterComment()); 80 | return this; 81 | } 82 | 83 | public WriterCell replaceStyle(Style style) { 84 | this.style = style; 85 | return this; 86 | } 87 | 88 | public WriterCell style(UnaryOperator f) { 89 | if (f != null) { 90 | // Create new copied style, not using the current reference 91 | Style.StyleBuilder builder = Style.builder(this.style); 92 | f.apply(builder); 93 | this.style = builder.build(); 94 | } 95 | return this; 96 | } 97 | 98 | public WriterCell constraint(Constraint constraint) { 99 | this.constraint = constraint; 100 | return this; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/core/WriterComment.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.core; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter(AccessLevel.PACKAGE) 8 | @Setter(AccessLevel.PACKAGE) 9 | public class WriterComment { 10 | private String content; 11 | private String author; 12 | private int rowOffset; 13 | private int colOffset; 14 | private int rowSpan = 2; 15 | private int colSpan = 1; 16 | 17 | WriterComment() { 18 | } 19 | 20 | public WriterComment content(String s) { 21 | this.content = s; 22 | return this; 23 | } 24 | 25 | public WriterComment author(String s) { 26 | this.author = s; 27 | return this; 28 | } 29 | 30 | public WriterComment rowOffset(int i) { 31 | this.rowOffset = i; 32 | return this; 33 | } 34 | 35 | public WriterComment colOffset(int i) { 36 | this.colOffset = i; 37 | return this; 38 | } 39 | 40 | public WriterComment rowSpan(int i) { 41 | if (i > 0) { 42 | this.rowSpan = i; 43 | } 44 | return this; 45 | } 46 | 47 | public WriterComment colSpan(int i) { 48 | if (i > 0) { 49 | this.colSpan = i; 50 | } 51 | return this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/style/Border.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.style; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.apache.poi.ss.usermodel.BorderStyle; 6 | import org.apache.poi.ss.usermodel.IndexedColors; 7 | 8 | @Getter 9 | @Setter 10 | public class Border { 11 | 12 | private BorderSide side = BorderSide.NONE; 13 | private StyleColor color = StyleColor.fromPredefined(IndexedColors.BLACK); 14 | private BorderStyle borderStyle = BorderStyle.THIN; 15 | 16 | public Border side(BorderSide side) { 17 | this.side = side; 18 | return this; 19 | } 20 | 21 | public Border hexColor(String hex) { 22 | this.color = StyleColor.fromHex(hex); 23 | return this; 24 | } 25 | 26 | public Border rgbColor(int r, int g, int b) { 27 | this.color = StyleColor.fromRGB(r, g, b); 28 | return this; 29 | } 30 | 31 | public Border color(IndexedColors predefinedColor) { 32 | this.color = StyleColor.fromPredefined(predefinedColor); 33 | return this; 34 | } 35 | 36 | public Border style(BorderStyle borderStyle) { 37 | this.borderStyle = borderStyle; 38 | return this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/style/BorderSide.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.style; 2 | 3 | public enum BorderSide { 4 | LEFT, 5 | TOP, 6 | RIGHT, 7 | BOTTOM, 8 | FULL, 9 | NONE 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/nambach/excelutil/style/CacheStyle.java: -------------------------------------------------------------------------------- 1 | package io.github.nambach.excelutil.style; 2 | 3 | import lombok.Getter; 4 | import org.apache.poi.hssf.usermodel.HSSFWorkbook; 5 | import org.apache.poi.ss.usermodel.CellStyle; 6 | import org.apache.poi.ss.usermodel.Workbook; 7 | import org.apache.poi.xssf.streaming.SXSSFWorkbook; 8 | import org.apache.poi.xssf.usermodel.XSSFWorkbook; 9 | 10 | import java.util.Arrays; 11 | import java.util.Collection; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.stream.Collectors; 15 | 16 | @Getter 17 | public class CacheStyle { 18 | private final Node root = new Node<>("root", null); 19 | private final Workbook workbook; 20 | 21 | private final StyleHandler handler; 22 | 23 | public CacheStyle(Workbook workbook) { 24 | this.workbook = workbook; 25 | 26 | if (workbook instanceof XSSFWorkbook) { 27 | XSSFWorkbook wb = (XSSFWorkbook) workbook; 28 | handler = new XSSFStyleHandler(wb, new XSSFColorCache(wb)); 29 | 30 | } else if (workbook instanceof HSSFWorkbook) { 31 | HSSFWorkbook wb = (HSSFWorkbook) workbook; 32 | handler = new HSSFStyleHandler(wb, new HSSFColorCache(wb, HSSFColorCache.Policy.USE_MOST_SIMILAR)); 33 | 34 | } else if (workbook instanceof SXSSFWorkbook) { 35 | SXSSFWorkbook wb = (SXSSFWorkbook) workbook; 36 | handler = new XSSFStyleHandler(wb.getXSSFWorkbook(), new XSSFColorCache(wb.getXSSFWorkbook())); 37 | 38 | } else { 39 | throw new RuntimeException("Unsupported workbook type"); 40 | } 41 | } 42 | 43 | public void setHSSFColorPolicy(HSSFColorCache.Policy policy) { 44 | if (handler instanceof HSSFStyleHandler) { 45 | ((HSSFStyleHandler) handler).colorCache.setPolicy(policy); 46 | } 47 | } 48 | 49 | public String printTotalStyle() { 50 | int total = root.countAllChildren(); 51 | String report = String.format("%d %s created.", total, total > 1 ? "styles were" : "style was"); 52 | 53 | int totalColors = handler.countColors(); 54 | String colorReport = String.format("%d %s created.", totalColors, totalColors > 1 ? "colors were" : "color was"); 55 | report += "\n" + colorReport; 56 | 57 | return report; 58 | } 59 | 60 | public CellStyle accumulate(Style... styles) { 61 | return accumulate(Arrays.asList(styles)); 62 | } 63 | 64 | public CellStyle accumulate(Collection