├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .mvn ├── extensions.xml └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── renovate.json └── src ├── main └── java │ └── io │ └── github │ └── helpermethod │ └── zipforge │ ├── DirectoryNode.java │ ├── FileNode.java │ ├── Node.java │ ├── NodeGroup.java │ ├── Visitor.java │ ├── ZipFileVisitor.java │ ├── ZipForge.java │ └── ZipForgeException.java └── test └── java └── io └── github └── helpermethod └── zipforge └── ZipForgeTest.java /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - README.md 8 | pull_request: 9 | branches: 10 | - main 11 | paths-ignore: 12 | - README.md 13 | jobs: 14 | build: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - name: Check out repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Set up Java 22 | uses: actions/setup-java@v4 23 | with: 24 | distribution: zulu 25 | java-version: | 26 | 8 27 | 21 28 | cache: maven 29 | - name: Cache Sonar 30 | uses: actions/cache@v4 31 | with: 32 | path: ~/.sonar/cache 33 | key: ${{ runner.os }}-sonar 34 | restore-keys: ${{ runner.os }}-sonar 35 | - name: Build 36 | run: SONAR_TOKEN=${{ secrets.SONAR_TOKEN }} ./mvnw --no-transfer-progress verify sonar:sonar 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: Release version 7 | required: true 8 | jobs: 9 | release: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Set up Java 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: zulu 20 | java-version: | 21 | 8 22 | 21 23 | cache: maven 24 | - name: Build 25 | env: 26 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 27 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 28 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 29 | JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }} 30 | JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }} 31 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: ./mvnw --no-transfer-progress -Prelease -Drevision=${{ inputs.version }} deploy jreleaser:full-release 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | target/ 3 | .mvn/wrapper/maven-wrapper.jar 4 | 5 | .idea/ 6 | *.iml 7 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.apache.maven.extensions 4 | maven-build-cache-extension 5 | 1.0.1 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-beta-4/apache-maven-4.0.0-beta-4-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌋 ZIP Forge 2 | 3 | [![CI](https://github.com/helpermethod/zip-forge/actions/workflows/ci.yml/badge.svg)](https://github.com/helpermethod/zip-forge/actions/workflows/ci.yml) 4 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=io.github.helpermethod%3Azip-forge&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=io.github.helpermethod%3Azip-forge) 5 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=io.github.helpermethod%3Azip-forge&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=io.github.helpermethod%3Azip-forge) 6 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=io.github.helpermethod%3Azip-forge&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=io.github.helpermethod%3Azip-forge) 7 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=io.github.helpermethod%3Azip-forge&metric=coverage)](https://sonarcloud.io/summary/new_code?id=io.github.helpermethod%3Azip-forge) 8 | 9 | A tiny, formatter-friendly Java DSL for creating ZIP files. 10 | 11 | # :sparkles: Features 12 | 13 | ## :pinching_hand: Tiny 14 | 15 | The ZIP Forge API consists of only 3 methods. 16 | 17 | ## :clipboard: Formatter-friendly 18 | 19 | Applying a code formatter like [palantir-java-format](https://github.com/palantir/palantir-java-format) won't mess up ZIP Forge's indentation. 20 | 21 | ## :package: No external dependencies 22 | 23 | ZIP Forge is based on Java's [ZIP File System Provider](https://docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/zipfilesystemprovider.html) and requires no external dependencies. 24 | 25 | ## :jigsaw: Modular 26 | 27 | ZIP Forge is published as a Java 9 module but compatible with Java 8. 28 | 29 | # :hammer_and_wrench: Installation 30 | 31 | ## Maven 32 | 33 | ```xml 34 | 35 | io.github.helpermethod 36 | zip-forge 37 | 1.0.1 38 | 39 | ``` 40 | 41 | ## Gradle 42 | 43 | ```groovy 44 | implementation 'io.github.helpermethod:zip-forge:1.0.1' 45 | ``` 46 | 47 | ## Gradle (Kotlin) 48 | 49 | ```kotlin 50 | implementation("io.github.helpermethod:zip-forge:1.0.1") 51 | ``` 52 | 53 | # :mag: Usage 54 | 55 | ## Java 56 | 57 | The following code snippet calls `createZipFile` to create the ZIP file at the given location. 58 | It uses the `file` and `directory` methods to create files and directories within the context of the ZIP file. 59 | 60 | > [!WARNING] 61 | > `file` and `directory` should never be used outside of `createZipFile`'s or `directory`'s context. 62 | 63 | ```java 64 | import java.nio.charset.StandardCharsets; 65 | 66 | import static io.github.helpermethod.zipforge.ZipForge.createZipFile; 67 | import static io.github.helpermethod.zipforge.ZipForge.file; 68 | import static io.github.helpermethod.zipforge.ZipForge.directory; 69 | import static java.nio.charset.StandardCharsets.UTF_8; 70 | 71 | class ZipForgeDemo { 72 | public static void main(String[] args) throws IOException { 73 | // creates a ZIP file named demo.zip in the /home/helpermethod directory 74 | createZipFile(Paths.get("/home/helpermethod/demo.zip"), () -> { 75 | // the file content can be specified as a String... 76 | file("a.txt", "a"); 77 | directory("d", () -> { 78 | // ... or a byte[]... 79 | file("b.txt", "b".getBytes(UTF_8)); 80 | // ... or a Path 81 | file("c.bin", Paths.get("c.bin")); 82 | // directories can be nested 83 | directory("e", () -> { 84 | file("f.txt", "f"); 85 | }); 86 | }); 87 | }); 88 | } 89 | } 90 | ``` 91 | 92 | The above code results in a ZIP file with the following structure. 93 | 94 | ``` 95 | Archive: demo.zip 96 | Length Date Time Name 97 | --------- ---------- ----- ---- 98 | 0 07-11-2023 15:39 d/ 99 | 0 07-11-2023 15:39 d/e/ 100 | 1 07-11-2023 15:39 a.txt 101 | 1 07-11-2023 15:39 d/b.txt 102 | 1 07-11-2023 15:39 d/c.bin 103 | 1 07-11-2023 15:39 d/e/f.txt 104 | --------- ------- 105 | 4 6 files 106 | ``` 107 | 108 | ## Kotlin 109 | 110 | The same example written in Kotlin. It uses the same API as the Java version. 111 | 112 | ```kotlin 113 | import io.github.helpermethod.zipforge.ZipForge.createZipFile 114 | import io.github.helpermethod.zipforge.ZipForge.directory 115 | import io.github.helpermethod.zipforge.ZipForge.file 116 | import kotlin.io.path.Path 117 | 118 | fun main() { 119 | createZipFile(Path("/home/helpermethod/demo.zip")) { 120 | file("a.txt", "a") 121 | directory("d") { 122 | file("b.txt", "b".toByteArray()) 123 | file("c.bin", Path("c.bin")) 124 | directory("e") { 125 | file("f.txt", "f") 126 | } 127 | } 128 | } 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.github.helpermethod 6 | zip-forge 7 | ${revision} 8 | 9 | ${project.artifactId} 10 | A tiny, formatter-friendly Java DSL for creating ZIP files. 11 | https://github.com/helpermethod/zip-forge 12 | 13 | 14 | 15 | The Apache License, Version 2.0 16 | https://www.apache.org/licenses/LICENSE-2.0.txt 17 | 18 | 19 | 20 | 21 | 22 | Oliver Weiler 23 | wo6980@gmail.com 24 | 25 | 26 | 27 | 28 | scm:git:${repository.url} 29 | scm:git:${repository.url} 30 | HEAD 31 | ${repository.url} 32 | 33 | 34 | 35 | 8 36 | 21 37 | ${java.version} 38 | ${java.version} 39 | ${java.testVersion} 40 | UTF-8 41 | git@github.com:helpermethod/zip-forge.git 42 | 1.0.0-SNAPSHOT 43 | helpermethod 44 | https://sonarcloud.io 45 | 46 | 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter 51 | 5.13.0 52 | test 53 | 54 | 55 | org.assertj 56 | assertj-core 57 | 3.27.3 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-deploy-plugin 68 | 3.1.4 69 | 70 | 71 | org.sonarsource.scanner.maven 72 | sonar-maven-plugin 73 | 5.1.0.4751 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-jar-plugin 78 | 3.4.2 79 | 80 | 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-compiler-plugin 86 | 3.14.0 87 | 88 | 89 | default-testCompile 90 | 91 | testCompile 92 | 93 | test-compile 94 | 95 | 96 | ${java.testVersion} 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-surefire-plugin 105 | 3.5.3 106 | 107 | 108 | ${java.testVersion} 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-toolchains-plugin 115 | 3.2.0 116 | 117 | 118 | 119 | ${java.version} 120 | 121 | 122 | 123 | 124 | 125 | 126 | toolchain 127 | 128 | 129 | 130 | 131 | 132 | com.diffplug.spotless 133 | spotless-maven-plugin 134 | 2.44.5 135 | 136 | 137 | 138 | 2.38.0 139 | 140 | 141 | 142 | 143 | false 144 | 4 145 | 146 | 147 | 148 | 149 | 150 | 151 | check 152 | 153 | 154 | 155 | 156 | 157 | org.jreleaser 158 | jreleaser-maven-plugin 159 | 1.18.0 160 | 161 | 162 | 163 | ALWAYS 164 | true 165 | 166 | 167 | 168 | 169 | 170 | ALWAYS 171 | https://s01.oss.sonatype.org/service/local 172 | true 173 | true 174 | target/staging-deploy 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ALWAYS 183 | conventional-commits 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | org.jacoco 192 | jacoco-maven-plugin 193 | 0.8.12 194 | 195 | 196 | default-prepare-agent 197 | 198 | prepare-agent 199 | 200 | 201 | 202 | default-report 203 | 204 | report 205 | 206 | 207 | 208 | XML 209 | 210 | 211 | 212 | 213 | 214 | 215 | org.moditect 216 | moditect-maven-plugin 217 | 1.2.2.Final 218 | 219 | 220 | add-module-info 221 | 222 | add-module-info 223 | 224 | package 225 | 226 | 227 | 228 | io.github.helpermethod.zipforge 229 | io.github.helpermethod.zipforge 230 | 231 | 232 | true 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | release 243 | 244 | local::file:./target/staging-deploy 245 | 246 | 247 | 248 | 249 | org.apache.maven.plugins 250 | maven-javadoc-plugin 251 | 3.11.2 252 | 253 | 254 | attach-javadocs 255 | 256 | jar 257 | 258 | 259 | true 260 | 261 | 262 | 263 | 264 | 265 | org.apache.maven.plugins 266 | maven-source-plugin 267 | 3.3.1 268 | 269 | 270 | attach-sources 271 | 272 | jar 273 | 274 | 275 | true 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/DirectoryNode.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Path; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public class DirectoryNode implements Node { 11 | private final Path path; 12 | private final List children = new ArrayList<>(); 13 | 14 | DirectoryNode(Path path) { 15 | this.path = path; 16 | } 17 | 18 | public void file(Path path, InputStream content) { 19 | children.add(new FileNode(path, content)); 20 | } 21 | 22 | public void directory(DirectoryNode directory) { 23 | children.add(directory); 24 | } 25 | 26 | @Override 27 | public void accept(Visitor visitor) throws IOException { 28 | visitor.visit(this); 29 | } 30 | 31 | Path path() { 32 | return path; 33 | } 34 | 35 | List children() { 36 | return Collections.unmodifiableList(children); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/FileNode.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Path; 6 | 7 | class FileNode implements Node { 8 | private final Path path; 9 | private final InputStream content; 10 | 11 | FileNode(Path path, InputStream content) { 12 | this.path = path; 13 | this.content = content; 14 | } 15 | 16 | @Override 17 | public void accept(Visitor visitor) throws IOException { 18 | visitor.visit(this); 19 | } 20 | 21 | Path path() { 22 | return path; 23 | } 24 | 25 | InputStream content() { 26 | return content; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/Node.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | 5 | interface Node { 6 | void accept(Visitor visitor) throws IOException; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/NodeGroup.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | public interface NodeGroup { 4 | void addNodes(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/Visitor.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | 5 | interface Visitor { 6 | void visit(FileNode file) throws IOException; 7 | 8 | void visit(DirectoryNode directory) throws IOException; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/ZipFileVisitor.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.FileSystem; 6 | import java.nio.file.Files; 7 | import java.nio.file.StandardCopyOption; 8 | 9 | class ZipFileVisitor implements Visitor { 10 | 11 | private final FileSystem zipFileSystem; 12 | 13 | ZipFileVisitor(FileSystem zipFileSystem) { 14 | this.zipFileSystem = zipFileSystem; 15 | } 16 | 17 | @Override 18 | public void visit(FileNode file) throws IOException { 19 | try (InputStream content = file.content()) { 20 | Files.copy(content, zipFileSystem.getPath(file.path().toString()), StandardCopyOption.REPLACE_EXISTING); 21 | } 22 | } 23 | 24 | @Override 25 | public void visit(DirectoryNode directory) throws IOException { 26 | Files.createDirectories(zipFileSystem.getPath(directory.path().toString())); 27 | 28 | for (Node node : directory.children()) { 29 | node.accept(this); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/ZipForge.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import static java.nio.charset.StandardCharsets.UTF_8; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.ArrayDeque; 14 | import java.util.Deque; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | public class ZipForge { 19 | private static final ThreadLocal> nodeDeque = ThreadLocal.withInitial(ArrayDeque::new); 20 | 21 | private ZipForge() {} 22 | 23 | public static Path createZipFile(Path path, NodeGroup nodeGroup) throws IOException { 24 | DirectoryNode rootNode = new DirectoryNode(Paths.get("")); 25 | 26 | try { 27 | nodeDeque.get().addLast(rootNode); 28 | nodeGroup.addNodes(); 29 | } finally { 30 | nodeDeque.remove(); 31 | } 32 | 33 | Map env = new HashMap<>(); 34 | env.put("create", "true"); 35 | 36 | try (FileSystem zipFileSystem = FileSystems.newFileSystem(URI.create("jar:" + path.toUri()), env)) { 37 | new ZipFileVisitor(zipFileSystem).visit(rootNode); 38 | } 39 | 40 | return path; 41 | } 42 | 43 | public static void file(String name, String content) { 44 | file(name, content.getBytes(UTF_8)); 45 | } 46 | 47 | public static void file(String name, byte[] content) { 48 | nodeDeque.get().getLast().file(path(name), new ByteArrayInputStream(content)); 49 | } 50 | 51 | public static void file(String name, Path content) { 52 | try { 53 | nodeDeque.get().getLast().file(path(name), Files.newInputStream(content)); 54 | } catch (IOException e) { 55 | throw new ZipForgeException(e); 56 | } 57 | } 58 | 59 | public static void directory(String name, NodeGroup nodeGroup) { 60 | DirectoryNode directoryNode = new DirectoryNode(path(name)); 61 | nodeDeque.get().getLast().directory(directoryNode); 62 | 63 | nodeDeque.get().addLast(directoryNode); 64 | nodeGroup.addNodes(); 65 | nodeDeque.get().removeLast(); 66 | } 67 | 68 | private static Path path(String name) { 69 | return nodeDeque.get().getLast().path().resolve(name); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/helpermethod/zipforge/ZipForgeException.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import java.io.IOException; 4 | 5 | public class ZipForgeException extends RuntimeException { 6 | public ZipForgeException(IOException e) { 7 | super(e); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/io/github/helpermethod/zipforge/ZipForgeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.helpermethod.zipforge; 2 | 3 | import static io.github.helpermethod.zipforge.ZipForge.createZipFile; 4 | import static io.github.helpermethod.zipforge.ZipForge.directory; 5 | import static io.github.helpermethod.zipforge.ZipForge.file; 6 | import static java.nio.charset.StandardCharsets.UTF_8; 7 | import static java.nio.file.StandardOpenOption.CREATE_NEW; 8 | import static java.nio.file.StandardOpenOption.WRITE; 9 | import static java.util.function.Predicate.not; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | import static org.junit.jupiter.params.provider.Arguments.arguments; 13 | 14 | import java.io.IOException; 15 | import java.nio.ByteBuffer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.List; 19 | import java.util.stream.Stream; 20 | import java.util.zip.ZipEntry; 21 | import java.util.zip.ZipFile; 22 | import org.junit.jupiter.api.DisplayNameGeneration; 23 | import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; 24 | import org.junit.jupiter.api.Nested; 25 | import org.junit.jupiter.api.Test; 26 | import org.junit.jupiter.api.extension.ExtensionContext; 27 | import org.junit.jupiter.api.io.TempDir; 28 | import org.junit.jupiter.params.ParameterizedTest; 29 | import org.junit.jupiter.params.provider.Arguments; 30 | import org.junit.jupiter.params.provider.ArgumentsProvider; 31 | import org.junit.jupiter.params.provider.ArgumentsSource; 32 | 33 | @DisplayNameGeneration(ReplaceUnderscores.class) 34 | class ZipForgeTest { 35 | 36 | @Nested 37 | class CreateZipFile { 38 | 39 | @ArgumentsSource(ZipFileEntries.class) 40 | @ParameterizedTest 41 | void should_create_a_zip_file_with_correct_entries( 42 | NodeGroup nodeGroup, List entries, @TempDir Path tempDir) throws IOException { 43 | var location = tempDir.resolve("test.zip"); 44 | 45 | createZipFile(location, nodeGroup); 46 | 47 | try (var zipFile = new ZipFile(location.toFile())) { 48 | assertThat(zipFile.stream()).extracting("name").isEqualTo(entries); 49 | } 50 | } 51 | 52 | @ArgumentsSource(ZipFileContents.class) 53 | @ParameterizedTest 54 | void should_create_a_zip_file_with_correct_contents( 55 | NodeGroup nodeGroup, List fileContents, @TempDir Path tempDir) throws IOException { 56 | var location = tempDir.resolve("test.zip"); 57 | 58 | createZipFile(location, nodeGroup); 59 | 60 | try (var zipFile = new ZipFile(location.toFile())) { 61 | var contents = zipFile.stream() 62 | .filter(not(ZipEntry::isDirectory)) 63 | .map(e -> { 64 | try { 65 | return zipFile.getInputStream(e).readAllBytes(); 66 | } catch (IOException ex) { 67 | throw new AssertionError(ex); 68 | } 69 | }) 70 | .toList(); 71 | 72 | assertThat(contents).containsExactlyElementsOf(fileContents); 73 | } 74 | } 75 | 76 | static class ZipFileEntries implements ArgumentsProvider { 77 | @Override 78 | public Stream provideArguments(ExtensionContext context) { 79 | return Stream.of( 80 | arguments( 81 | (NodeGroup) () -> { 82 | file("a.txt", "a"); 83 | }, 84 | List.of("a.txt")), 85 | arguments( 86 | (NodeGroup) () -> { 87 | file("a.txt", "a"); 88 | file("b.txt", "b"); 89 | }, 90 | List.of("a.txt", "b.txt")), 91 | arguments( 92 | (NodeGroup) () -> { 93 | directory("d", () -> {}); 94 | }, 95 | List.of("d/")), 96 | arguments((NodeGroup) () -> directory("d/", () -> {}), List.of("d/")), 97 | arguments( 98 | (NodeGroup) () -> directory("d", () -> { 99 | file("a.txt", "a"); 100 | }), 101 | List.of("d/", "d/a.txt")), 102 | arguments( 103 | (NodeGroup) () -> directory("d", () -> { 104 | file("a.txt", "a"); 105 | file("b.txt", "b"); 106 | }), 107 | List.of("d/", "d/a.txt", "d/b.txt")), 108 | arguments( 109 | (NodeGroup) () -> directory("d", () -> { 110 | directory("e", () -> { 111 | file("a.txt", "a"); 112 | }); 113 | }), 114 | List.of("d/", "d/e/", "d/e/a.txt")), 115 | arguments( 116 | (NodeGroup) () -> directory("d", () -> { 117 | directory("e", () -> { 118 | file("a.txt", "a"); 119 | file("b.txt", "b"); 120 | }); 121 | }), 122 | List.of("d/", "d/e/", "d/e/a.txt", "d/e/b.txt")), 123 | arguments( 124 | (NodeGroup) () -> { 125 | file("a.txt", "a"); 126 | directory("d", () -> { 127 | directory("e", () -> { 128 | file("b.txt", "b"); 129 | file("c.txt", "c"); 130 | }); 131 | }); 132 | }, 133 | List.of("a.txt", "d/", "d/e/", "d/e/b.txt", "d/e/c.txt")), 134 | arguments( 135 | (NodeGroup) () -> { 136 | file("a.txt", "a"); 137 | directory("d", () -> {}); 138 | }, 139 | List.of("a.txt", "d/")), 140 | arguments( 141 | (NodeGroup) () -> { 142 | directory("d", () -> {}); 143 | directory("e", () -> {}); 144 | }, 145 | List.of("d/", "e/")), 146 | arguments((NodeGroup) () -> {}, List.of())); 147 | } 148 | } 149 | 150 | static class ZipFileContents implements ArgumentsProvider { 151 | @Override 152 | public Stream provideArguments(ExtensionContext context) { 153 | return Stream.of( 154 | arguments((NodeGroup) () -> file("a.txt", "a"), List.of("a".getBytes(UTF_8))), 155 | arguments( 156 | (NodeGroup) () -> { 157 | file("a.txt", "a"); 158 | file("b.txt", "b"); 159 | }, 160 | List.of("a".getBytes(UTF_8), "b".getBytes(UTF_8))), 161 | arguments( 162 | (NodeGroup) () -> { 163 | directory("d", () -> { 164 | file("a.txt", "a"); 165 | }); 166 | }, 167 | List.of("a".getBytes(UTF_8))), 168 | arguments( 169 | (NodeGroup) () -> { 170 | directory("d", () -> { 171 | directory("e", () -> { 172 | file("a", "a"); 173 | }); 174 | }); 175 | }, 176 | List.of("a".getBytes(UTF_8))), 177 | arguments( 178 | (NodeGroup) () -> { 179 | file("a.txt", "a".getBytes(UTF_8)); 180 | }, 181 | List.of("a".getBytes(UTF_8))), 182 | arguments((NodeGroup) () -> {}, List.of())); 183 | } 184 | } 185 | } 186 | 187 | @Nested 188 | class File { 189 | @Test 190 | void should_support_large_files(@TempDir Path tempDir) throws IOException { 191 | var largeFile = tempDir.resolve("large-file.bin"); 192 | 193 | try (var twoGigaByteFile = Files.newByteChannel(largeFile, CREATE_NEW, WRITE)) { 194 | long twoGigaBytesInBytes = 2L * 1024L * 1024L * 1024L; 195 | 196 | twoGigaByteFile.position(twoGigaBytesInBytes - 1); 197 | twoGigaByteFile.write(ByteBuffer.wrap(new byte[] {0})); 198 | } 199 | 200 | assertThat(createZipFile(tempDir.resolve("test.zip"), () -> file("a.bin", largeFile))) 201 | .exists(); 202 | } 203 | 204 | @Test 205 | void should_throw_an_exception_if_the_path_does_not_exist(@TempDir Path tempDir) throws IOException { 206 | assertThatThrownBy(() -> createZipFile(tempDir.resolve("test.zip"), () -> file("a.bin", Path.of("a.bin")))) 207 | .isInstanceOf(ZipForgeException.class) 208 | .hasMessage("java.nio.file.NoSuchFileException: a.bin"); 209 | } 210 | } 211 | } 212 | --------------------------------------------------------------------------------