├── .gitattributes ├── .github └── workflows │ ├── release.yaml │ └── workflow.yaml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── PrepareRelease.java ├── README.md ├── eclipse-code-formatter.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── com │ └── romanboehm │ └── jsonwheel │ └── JsonWheel.java └── test ├── java └── com │ └── romanboehm │ └── jsonwheel │ ├── ArrayTest.java │ ├── BooleanTest.java │ ├── ComparisonTest.java │ ├── JsonWheelTestMatrix.java │ ├── NullTest.java │ ├── NumberTest.java │ ├── ObjectTest.java │ ├── StringTest.java │ └── WheelNodeTest.java └── resources └── jsondotorg └── pass ├── pass1.json ├── pass2.json └── pass3.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Cf. https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings 2 | # Set the default behavior, in case people don't have core.autocrlf set. 3 | * text=auto 4 | 5 | # Declare files that will always have CRLF line endings on checkout. 6 | *.cmd text eol=crlf -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | releaseVersion: 6 | description: "Version to use for the release." 7 | required: true 8 | 9 | jobs: 10 | prepare-release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v3 15 | - name: set up JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'adopt' 20 | server-id: github 21 | - name: setup git 22 | run: | 23 | git config user.email "actions@github.com" 24 | git config user.name "GitHub Actions" 25 | - name: verify with Maven 26 | run: | 27 | ./mvnw --batch-mode verify 28 | - name: prepare release 29 | run: | 30 | ./mvnw --batch-mode -Darguments=-DskipTests release:prepare -DreleaseVersion=${{ inputs.releaseVersion }} 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | do-release: 34 | needs: prepare-release 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: checkout 38 | uses: actions/checkout@v3 39 | with: 40 | ref: v${{ inputs.releaseVersion }} 41 | fetch-depth: 0 42 | - name: set up JDK 17 43 | uses: actions/setup-java@v3 44 | with: 45 | java-version: '17' 46 | distribution: 'adopt' 47 | - name: create source distribution 48 | run: | 49 | ./mvnw --batch-mode package -DskipTests 50 | - name: release with JReleaser 51 | run: | 52 | ./mvnw --batch-mode jreleaser:release 53 | env: 54 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | name: workflow 2 | on: [push] 3 | 4 | jobs: 5 | build-and-verify: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: checkout 9 | uses: actions/checkout@v3 10 | - name: set up JDK 17 11 | uses: actions/setup-java@v3 12 | with: 13 | java-version: '17' 14 | distribution: 'adopt' 15 | - name: verify with Maven 16 | run: | 17 | ./mvnw --batch-mode verify -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | .idea 4 | /trace.log 5 | out 6 | release.properties 7 | /pom.xml.releaseBackup 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romanboehm/jsonwheel/2cae451c83acdde10e74d9f9a42ff740140c36cf/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.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/3.8.3/apache-maven-3.8.3-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.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 | -------------------------------------------------------------------------------- /PrepareRelease.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import java.nio.file.Files; 3 | import java.nio.file.Path; 4 | import java.nio.file.StandardOpenOption; 5 | 6 | class PrepareRelease { 7 | 8 | public static void main(String... args) throws IOException { 9 | var src = Path.of(args[0]); 10 | var dest = Path.of(args[1]); 11 | var withoutPackageDecl = Files.readAllLines(src).stream() 12 | .skip(2) 13 | .toList(); 14 | Files.createDirectories(dest.getParent()); 15 | Files.write( 16 | dest, 17 | withoutPackageDecl, 18 | StandardOpenOption.CREATE 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Wheel 2 | 3 | Have you ever written scripts in Java 11+ and needed to operate on some JSON string? Have you ever needed to extract *just that one deeply-nested value* of an `application/json` response without modelling the whole hierarchy? Have you 4 | ever felt someone should indeed reinvent that wheel and provide the JVM universe with yet another JSON deserializer? 5 | 6 | If the answer to any of those is "yes", then look no further, JSON Wheel's got you covered. 7 | 8 | **It's a 250 lines single-source-file hackable JSON deserializer written in plain Java.** 9 | 10 | If the answer is "no": Fine by me, I still had fun writing the thing. :) 11 | 12 | ## Features 13 | 14 | * very small codebase 15 | * hackable 16 | * dependency-less 17 | * compatible to JDK 8+ 18 | * not toooo slow 19 | 20 | ## Limitations 21 | 22 | * deserialization-only 23 | * not typed, except for the `WheelNode` type (which you may cast to "value" types, see below) 24 | * Java number types currently offered: `Double`, `BigDecimal`, `Integer`, `Long`, `BigInteger` 25 | * only partially safe against malformed input 26 | 27 | ## Usage 28 | ### Within Your Script 29 | 1. Copy the **content** of [the desired version of JsonWheel.java](https://github.com/romanboehm/jsonwheel/releases) as-is into your existing script. 30 | 2. Remove imports if colliding with your pre-existing ones. 31 | ### Within Your Project 32 | 1. Copy [the desired version of JsonWheel.java](https://github.com/romanboehm/jsonwheel/releases) as a file into your project's source directory, e.g. `/src/main/java`. 33 | 2. Adjust the package declaration, i.e. introduce one, if needed. 34 | ### With JBang's `//SOURCES` 35 | 1. Create a JBang script, e.g. with `jbang init`. 36 | 2. Use the `//SOURCES https://github.com/romanboehm/jsonwheel/blob/main/src/main/java/com/romanboehm/jsonwheel/JsonWheel.java` directive to include the JSON Wheel source 37 | 38 | Then you can ... 39 | 40 | ### 1) Deserialize JSON objects 41 | 42 | ```java 43 | var json = """ 44 | { 45 | "foo": 1, 46 | "bar": "baz", 47 | "qux": null 48 | }"""; 49 | 50 | var node = JsonWheel.read(json); 51 | var foo = node.get("foo").val(Integer.class); // 1 52 | var bar = node.get("bar").val(String.class); // "baz" 53 | var qux = node.get("qux").val(String.class); // null 54 | ``` 55 | ### 2) Deserialize JSON arrays 56 | 57 | ```java 58 | var json = """ 59 | [ 60 | 1, 61 | 2, 62 | 3 63 | ]"""; 64 | 65 | var node = JsonWheel.read(json); 66 | var list = node.elements() 67 | .stream() 68 | .map(e -> e.val(Integer.class)) 69 | .toList(); // 1 2 3 70 | ``` 71 | 72 | ### 3) Deserialize "complex" JSON 73 | 74 | ```java 75 | var json = """ 76 | { 77 | "foo": { 78 | "bar" : [ 79 | 1, 80 | 2, 81 | 3 82 | ], 83 | "baz": "qux" 84 | } 85 | }"""; 86 | 87 | var node = JsonWheel.read(json); 88 | var bar = node.get("foo").get("bar").elements() 89 | .stream() 90 | .map(e -> e.val(Integer.class)) 91 | .toList(); // 1 2 3 92 | var baz = node.get("foo").get("baz").val(String.class); // "qux" 93 | ``` 94 | 95 | ## Release 96 | Run the [_release_ GitHub workflow](https://github.com/romanboehm/jsonwheel/actions/workflows/release.yaml) with the desired version. 97 | -------------------------------------------------------------------------------- /eclipse-code-formatter.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 99 | 101 | 102 | 103 | 104 | 105 | 107 | 109 | 111 | 113 | 114 | 115 | 116 | 118 | 119 | 121 | 122 | 123 | 124 | 125 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 143 | 144 | 146 | 147 | 148 | 150 | 152 | 153 | 154 | 156 | 158 | 160 | 161 | 162 | 163 | 165 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | 181 | 182 | 184 | 185 | 186 | 187 | 189 | 190 | 192 | 193 | 194 | 196 | 198 | 199 | 201 | 203 | 204 | 206 | 207 | 209 | 211 | 212 | 214 | 215 | 216 | 217 | 218 | 219 | 221 | 222 | 224 | 225 | 226 | 228 | 229 | 231 | 233 | 235 | 236 | 238 | 239 | 240 | 242 | 244 | 245 | 246 | 247 | 250 | 252 | 253 | 254 | 255 | 257 | 259 | 260 | 261 | 263 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 276 | 278 | 280 | 282 | 283 | 284 | 285 | 286 | 287 | 289 | 290 | 291 | 292 | 293 | 294 | 297 | 298 | 300 | 301 | 303 | 304 | 305 | 306 | 307 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 317 | 318 | 319 | 320 | 321 | 323 | 325 | 327 | 328 | 331 | 332 | 333 | 334 | 335 | 336 | 338 | 339 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 350 | 351 | 352 | 354 | 356 | 358 | 359 | 361 | 362 | 363 | 365 | 366 | 367 | 369 | 371 | 373 | 374 | 375 | 378 | 380 | 382 | 384 | 386 | 387 | 388 | 390 | 392 | 393 | 394 | 396 | 398 | 399 | 400 | 401 | -------------------------------------------------------------------------------- /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 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /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 Maven Start Up Batch script 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 M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.romanboehm 7 | jsonwheel 8 | 1.2.1-SNAPSHOT 9 | jsonwheel 10 | A 250 lines single-source-file hackable JSON deserializer for the JVM. Reinventing the JSON wheel. 11 | 2022 12 | 13 | 14 | https://github.com/romanboehm/jsonwheel 15 | scm:git:https://github.com/romanboehm/jsonwheel.git 16 | HEAD 17 | 18 | 19 | 20 | UTF-8 21 | 3.9.0 22 | 8 23 | 17 24 | 17 25 | 2.22.2 26 | 2.22.2 27 | 5.8.2 28 | 3.22.0 29 | 2.13.2 30 | 3.1.0 31 | 1.5.1 32 | 3.0.0 33 | 2.22.0 34 | 35 | 36 | 37 | 38 | 39 | 40 | default 41 | 42 | true 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-compiler-plugin 49 | ${maven-compiler-plugin.version} 50 | 51 | ${maven-compiler-plugin.actual.release} 52 | ${maven-compiler-plugin.testRelease} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ide 61 | 62 | false 63 | 64 | idea.maven.embedder.version 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 3.9.0 73 | 74 | ${maven-compiler-plugin.intellij.release} 75 | ${maven-compiler-plugin.testRelease} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.junit.jupiter 86 | junit-jupiter 87 | ${junit-jupiter.version} 88 | test 89 | 90 | 91 | org.assertj 92 | assertj-core 93 | ${assertj-core.version} 94 | test 95 | 96 | 97 | com.fasterxml.jackson.jr 98 | jackson-jr-objects 99 | ${jackson-jr-objects.version} 100 | test 101 | 102 | 103 | 104 | 105 | 106 | 107 | net.revelc.code.formatter 108 | formatter-maven-plugin 109 | ${formatter-maven-plugin.version} 110 | 111 | ${project.basedir}/eclipse-code-formatter.xml 112 | 113 | 114 | 115 | 116 | validate 117 | 118 | 119 | 120 | 121 | 122 | maven-surefire-plugin 123 | ${maven-surefire-plugin.version} 124 | 125 | 126 | maven-failsafe-plugin 127 | ${maven-failsafe-plugin.version} 128 | 129 | 130 | org.codehaus.mojo 131 | exec-maven-plugin 132 | ${exec-maven-plugin.version} 133 | 134 | 135 | prepare-release 136 | package 137 | 138 | exec 139 | 140 | 141 | 142 | 143 | java 144 | 145 | PrepareRelease.java 146 | ${project.build.sourceDirectory}/com/romanboehm/jsonwheel/JsonWheel.java 147 | ${project.build.outputDirectory}/distributions/JsonWheel.java 148 | 149 | 150 | 151 | 152 | org.jreleaser 153 | jreleaser-maven-plugin 154 | ${jreleaser-maven-plugin.version} 155 | 156 | 157 | 158 | ${project.name} 159 | 160 | 161 | 162 | romanboehm 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | ${project.build.outputDirectory}/distributions/JsonWheel.java 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-release-plugin 180 | ${maven-release-plugin.version} 181 | 182 | v@{project.version} 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/main/java/com/romanboehm/jsonwheel/JsonWheel.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.BigInteger; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.function.Consumer; 11 | 12 | /** 13 | * Copyright (c) 2022 Roman Böhm. Subject to the Apache License 2.0. 14 | *
15 | * See also https://github.com/romanboehm/jsonwheel. 16 | */ 17 | class JsonWheel { 18 | 19 | static WheelNode read(String json) { 20 | char[] chars = json.toCharArray(); 21 | return new Deserializer(chars).readInternal(); 22 | } 23 | 24 | static class JsonWheelException extends RuntimeException { 25 | JsonWheelException(String msg) { 26 | super(msg); 27 | } 28 | } 29 | 30 | static class WheelNode { 31 | Object inner; 32 | 33 | WheelNode setInner(Object inner) { 34 | this.inner = inner; 35 | return this; 36 | } 37 | 38 | List elements() { 39 | List list = new ArrayList<>(); 40 | for (Object o : ((List) inner)) { 41 | list.add(new WheelNode().setInner(o)); 42 | } 43 | return list; 44 | } 45 | 46 | WheelNode get(String key) { 47 | return new WheelNode().setInner(((Map) inner).get(key)); 48 | } 49 | 50 | T val(Class clazz) { 51 | return inner == null ? null : clazz.cast(inner); 52 | } 53 | } 54 | 55 | static class Deserializer { 56 | private static final List NUMBER_CHARS = Arrays.asList('+', '-', '.', 'e', 'E'); 57 | private static final Map ESCAPE_LOOKUP = new HashMap<>(); 58 | 59 | static { 60 | ESCAPE_LOOKUP.put('n', '\n'); 61 | ESCAPE_LOOKUP.put('b', '\b'); 62 | ESCAPE_LOOKUP.put('t', '\t'); 63 | ESCAPE_LOOKUP.put('f', '\f'); 64 | ESCAPE_LOOKUP.put('r', '\r'); 65 | ESCAPE_LOOKUP.put('/', '/'); 66 | ESCAPE_LOOKUP.put('\\', '\\'); 67 | ESCAPE_LOOKUP.put('"', '"'); 68 | } 69 | 70 | private final char[] chars; 71 | 72 | Deserializer(char[] chars) { 73 | this.chars = chars; 74 | } 75 | 76 | WheelNode readInternal() { 77 | WheelNode wheelNode = new WheelNode(); 78 | readValue(o -> wheelNode.setInner(o), 0); 79 | return wheelNode; 80 | } 81 | 82 | private int readValue(Consumer valueConsumer, int from) { 83 | switch (chars[from]) { 84 | case '{': 85 | Map map = new HashMap<>(); 86 | valueConsumer.accept(map); 87 | return readObjectValue(map, from); 88 | case '[': 89 | List list = new ArrayList<>(); 90 | valueConsumer.accept(list); 91 | return readArrayValue(list, from); 92 | case '"': 93 | int closingQuote = next('"', from + 1); 94 | valueConsumer.accept(parseString(from + 1, closingQuote - 1)); 95 | return closingQuote; 96 | case 'n': 97 | int nullEnd = readLiteral(from, "null"); 98 | valueConsumer.accept(null); 99 | return nullEnd; 100 | case 't': 101 | int trueEnd = readLiteral(from, "true"); 102 | valueConsumer.accept(true); 103 | return trueEnd; 104 | case 'f': 105 | int falseEnd = readLiteral(from, "false"); 106 | valueConsumer.accept(false); 107 | return falseEnd; 108 | default: 109 | int numberEnd = readNumber(from); 110 | valueConsumer.accept(parseNumber(from, numberEnd)); 111 | return numberEnd; 112 | } 113 | } 114 | 115 | private int readObjectValue(Map map, int from) { 116 | int next = next(from + 1); 117 | 118 | // Check if empty object literal. 119 | if (chars[next] == '}') { 120 | return next; 121 | } 122 | 123 | // Consume object literal's fields. 124 | int delim = from; 125 | do { 126 | int keyStart = next('"', delim) + 1; 127 | int keyEnd = next('"', keyStart) - 1; 128 | String key = parseString(keyStart, keyEnd); 129 | int colon = next(':', keyEnd); 130 | int valueStart = next(colon + 1); 131 | int valueEnd = readValue(v -> map.put(key, v), valueStart); 132 | delim = next(valueEnd + 1); 133 | } while (chars[delim] == ','); 134 | 135 | return delim; 136 | } 137 | 138 | private int readArrayValue(List list, int from) { 139 | int next = next(from + 1); 140 | 141 | // Check if empty array literal. 142 | if (chars[next] == ']') { 143 | return next; 144 | } 145 | 146 | // Consume array literal's fields. 147 | int delim = from; 148 | do { 149 | int valueEnd = readValue(v -> list.add(v), next(delim + 1)); 150 | delim = next(valueEnd + 1); 151 | } while (chars[delim] == ','); 152 | 153 | return delim; 154 | } 155 | 156 | private int readNumber(int from) { 157 | while (from < chars.length && (Character.isDigit(chars[from]) || NUMBER_CHARS.contains(chars[from]))) { 158 | from++; 159 | } 160 | return from - 1; 161 | } 162 | 163 | private int readLiteral(int from, String expected) { 164 | int to = from; 165 | while (to < chars.length && Character.isLetter(chars[to])) { 166 | to++; 167 | } 168 | String literal = new String(Arrays.copyOfRange(chars, from, to)); 169 | if (!literal.equals(expected)) { 170 | throw new JsonWheelException("Invalid literal '" + literal + "' at " + from); 171 | } 172 | return to - 1; 173 | } 174 | 175 | private int next(char c, int from) { 176 | char prev = '\0'; 177 | boolean isEscaped = false; 178 | for (; from < chars.length; from++) { 179 | isEscaped = prev == '\\' && !isEscaped; // This handles strings like "\\". 180 | char current = chars[from]; 181 | if (!isEscaped && c == current) { 182 | return from; 183 | } 184 | prev = current; 185 | } 186 | throw new JsonWheelException("Could not find " + c + ", checking from " + from); 187 | } 188 | 189 | private int next(int from) { 190 | for (; from < chars.length; from++) { 191 | if (!Character.isWhitespace(chars[from])) { 192 | return from; 193 | } 194 | } 195 | throw new JsonWheelException("Could not find non-whitespace, checking from " + from); 196 | } 197 | 198 | private Number parseNumber(int from, int to) { 199 | String n = new String(Arrays.copyOfRange(chars, from, to + 1)); 200 | try { 201 | if (n.contains(".") || n.toLowerCase().contains("e")) { 202 | BigDecimal bd = new BigDecimal(n); 203 | double dv = bd.doubleValue(); 204 | if (dv != Double.POSITIVE_INFINITY && dv != Double.NEGATIVE_INFINITY && bd.compareTo(BigDecimal.valueOf(dv)) == 0) { // n within 64 bit precision? 205 | return Double.parseDouble(n); 206 | } 207 | return bd; // Use arbitrary precision 208 | } 209 | BigInteger bi = new BigInteger(n); 210 | if (bi.compareTo(BigInteger.valueOf(bi.intValue())) == 0) { // n within 32 bit precision? 211 | return Integer.parseInt(n); 212 | } 213 | if (bi.compareTo(BigInteger.valueOf(bi.longValue())) == 0) { // n within 64 bit precision? 214 | return Long.parseLong(n); 215 | } 216 | return bi; // Use arbitrary precision 217 | } 218 | catch (NumberFormatException nfe) { 219 | throw new JsonWheelException("Invalid number literal " + n + " at " + from + ": " + nfe.getMessage()); 220 | } 221 | } 222 | 223 | private String parseString(int from, int to) { 224 | if (from < 0 || to >= chars.length) { 225 | throw new JsonWheelException("Out of bounds building String from " + from + " to " + to); 226 | } 227 | StringBuilder builder = new StringBuilder(); 228 | while (from <= to) { 229 | if (chars[from] == '\\' && from + 1 <= to) { 230 | from++; // Skip backslash. Then check 231 | // a) codepoint in u-syntax, or ... 232 | if (chars[from] == 'u') { 233 | int cpStart = from + 1; // Skip "u". 234 | int cpEnd = cpStart + 3; 235 | if (cpEnd > to) { 236 | throw new JsonWheelException("Invalid codepoint at " + from); 237 | } 238 | builder.appendCodePoint(Integer.parseInt(new String(Arrays.copyOfRange(chars, cpStart, cpEnd + 1)), 16)); 239 | from = cpEnd; 240 | } 241 | // b) other escaped characters for which we can use the lookup table. 242 | else { 243 | Character escapeLookup = ESCAPE_LOOKUP.get(chars[from]); 244 | if (escapeLookup == null) { 245 | throw new JsonWheelException("Invalid escape sequence at " + from + ": " + chars[from]); 246 | } 247 | builder.append(escapeLookup); 248 | } 249 | } 250 | else { 251 | builder.append(chars[from]); 252 | } 253 | from++; 254 | } 255 | return builder.toString(); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/ArrayTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import org.junit.jupiter.params.provider.Arguments; 12 | 13 | class ArrayTest extends JsonWheelTestMatrix { 14 | 15 | @Override 16 | List args() { 17 | return List.of( 18 | arguments(named("empty array", new Arg("[]", Collections.emptyList()))), 19 | arguments(named("array single string", new Arg("[\"foo\"]", List.of("foo")))), 20 | arguments(named("array single number", new Arg("[1.1]", List.of(1.1d)))), 21 | arguments(named("array single boolean", new Arg("[true]", List.of(true)))), 22 | arguments(named("array single null", new Arg("[null]", Collections.singletonList(null)))), 23 | arguments(named("array single object", new Arg("[{\"foo\": 1.1}]", List.of(Map.of("foo", 1.1d))))), 24 | arguments(named("array single array", new Arg("[[1.1]]", List.of(List.of(1.1d))))), 25 | arguments(named("array multiple strings", new Arg("[\"foo\", \"bar\"]", List.of("foo", "bar")))), 26 | arguments(named("array multiple numbers", new Arg("[1.1, 0.5]", List.of(1.1d, 0.5d)))), 27 | arguments(named("array multiple booleans", new Arg("[true, false]", List.of(true, false)))), 28 | arguments(named("array multiple nulls", new Arg("[null, null]", Arrays.asList(null, null)))), 29 | arguments(named("array multiple values with nulls", new Arg("[null, null, 1.1, 0.5]", Arrays.asList(null, null, 1.1d, 0.5d)))), 30 | arguments(named("array multiple objects", new Arg("[{\"foo\": 1.1}, {\"foo\": 0.5}]", List.of(Map.of("foo", 1.1d), Map.of("foo", 0.5d))))), 31 | arguments(named("array multiple arrays", new Arg("[[1.1, 0.5], [0.5, 1.1]]", List.of(List.of(1.1d, 0.5), List.of(0.5, 1.1)))))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/BooleanTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.params.provider.Arguments; 9 | 10 | class BooleanTest extends JsonWheelTestMatrix { 11 | 12 | @Override 13 | List args() { 14 | return List.of( 15 | arguments(named("true", new Arg("true", true))), 16 | arguments(named("false", new Arg("false", false)))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/ComparisonTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.io.IOException; 6 | import java.net.URISyntaxException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Paths; 9 | import java.util.List; 10 | 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.MethodSource; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | import com.fasterxml.jackson.jr.ob.JSON; 16 | 17 | class ComparisonTest { 18 | 19 | private static List jsonDotOrgPassJsons() throws URISyntaxException, IOException { 20 | try (var stream = Files.list(Paths.get(ComparisonTest.class.getResource("/jsondotorg/pass").toURI()))) { 21 | return stream.map(path -> { 22 | try { 23 | return Files.readString(path); 24 | } 25 | catch (IOException e) { 26 | throw new RuntimeException(e); 27 | } 28 | }).toList(); 29 | } 30 | } 31 | 32 | // Examples taken from https://json.org/example.html. 33 | @ParameterizedTest(name = ParameterizedTest.INDEX_PLACEHOLDER) 34 | @ValueSource(strings = { 35 | """ 36 | { 37 | "glossary": { 38 | "title": "example glossary", 39 | "GlossDiv": { 40 | "title": "S", 41 | "GlossList": { 42 | "GlossEntry": { 43 | "ID": "SGML", 44 | "SortAs": "SGML", 45 | "GlossTerm": "Standard Generalized Markup Language", 46 | "Acronym": "SGML", 47 | "Abbrev": "ISO 8879:1986", 48 | "GlossDef": { 49 | "para": "A meta-markup language, used to create markup languages such as DocBook.", 50 | "GlossSeeAlso": ["GML", "XML"] 51 | }, 52 | "GlossSee": "markup" 53 | } 54 | } 55 | } 56 | } 57 | }""", 58 | """ 59 | {"menu": { 60 | "id": "file", 61 | "value": "File", 62 | "popup": { 63 | "menuitem": [ 64 | {"value": "New", "onclick": "CreateNewDoc()"}, 65 | {"value": "Open", "onclick": "OpenDoc()"}, 66 | {"value": "Close", "onclick": "CloseDoc()"} 67 | ] 68 | } 69 | }}""", 70 | """ 71 | {"widget": { 72 | "debug": "on", 73 | "window": { 74 | "title": "Sample Konfabulator Widget", 75 | "name": "main_window", 76 | "width": 500, 77 | "height": 500 78 | }, 79 | "image": { 80 | "src": "Images/Sun.png", 81 | "name": "sun1", 82 | "hOffset": 250, 83 | "vOffset": 250, 84 | "alignment": "center" 85 | }, 86 | "text": { 87 | "data": "Click Here", 88 | "size": 36, 89 | "style": "bold", 90 | "name": "text1", 91 | "hOffset": 250, 92 | "vOffset": 100, 93 | "alignment": "center", 94 | "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" 95 | } 96 | }}""", 97 | """ 98 | {"web-app": { 99 | "servlet": [ 100 | { 101 | "servlet-name": "cofaxCDS", 102 | "servlet-class": "org.cofax.cds.CDSServlet", 103 | "init-param": { 104 | "configGlossary:installationAt": "Philadelphia, PA", 105 | "configGlossary:adminEmail": "ksm@pobox.com", 106 | "configGlossary:poweredBy": "Cofax", 107 | "configGlossary:poweredByIcon": "/images/cofax.gif", 108 | "configGlossary:staticPath": "/content/static", 109 | "templateProcessorClass": "org.cofax.WysiwygTemplate", 110 | "templateLoaderClass": "org.cofax.FilesTemplateLoader", 111 | "templatePath": "templates", 112 | "templateOverridePath": "", 113 | "defaultListTemplate": "listTemplate.htm", 114 | "defaultFileTemplate": "articleTemplate.htm", 115 | "useJSP": false, 116 | "jspListTemplate": "listTemplate.jsp", 117 | "jspFileTemplate": "articleTemplate.jsp", 118 | "cachePackageTagsTrack": 200, 119 | "cachePackageTagsStore": 200, 120 | "cachePackageTagsRefresh": 60, 121 | "cacheTemplatesTrack": 100, 122 | "cacheTemplatesStore": 50, 123 | "cacheTemplatesRefresh": 15, 124 | "cachePagesTrack": 200, 125 | "cachePagesStore": 100, 126 | "cachePagesRefresh": 10, 127 | "cachePagesDirtyRead": 10, 128 | "searchEngineListTemplate": "forSearchEnginesList.htm", 129 | "searchEngineFileTemplate": "forSearchEngines.htm", 130 | "searchEngineRobotsDb": "WEB-INF/robots.db", 131 | "useDataStore": true, 132 | "dataStoreClass": "org.cofax.SqlDataStore", 133 | "redirectionClass": "org.cofax.SqlRedirection", 134 | "dataStoreName": "cofax", 135 | "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver", 136 | "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon", 137 | "dataStoreUser": "sa", 138 | "dataStorePassword": "dataStoreTestQuery", 139 | "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';", 140 | "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log", 141 | "dataStoreInitConns": 10, 142 | "dataStoreMaxConns": 100, 143 | "dataStoreConnUsageLimit": 100, 144 | "dataStoreLogLevel": "debug", 145 | "maxUrlLength": 500}}, 146 | { 147 | "servlet-name": "cofaxEmail", 148 | "servlet-class": "org.cofax.cds.EmailServlet", 149 | "init-param": { 150 | "mailHost": "mail1", 151 | "mailHostOverride": "mail2"}}, 152 | { 153 | "servlet-name": "cofaxAdmin", 154 | "servlet-class": "org.cofax.cds.AdminServlet"}, 155 | { 156 | "servlet-name": "fileServlet", 157 | "servlet-class": "org.cofax.cds.FileServlet"}, 158 | { 159 | "servlet-name": "cofaxTools", 160 | "servlet-class": "org.cofax.cms.CofaxToolsServlet", 161 | "init-param": { 162 | "templatePath": "toolstemplates/", 163 | "log": 1, 164 | "logLocation": "/usr/local/tomcat/logs/CofaxTools.log", 165 | "logMaxSize": "", 166 | "dataLog": 1, 167 | "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log", 168 | "dataLogMaxSize": "", 169 | "removePageCache": "/content/admin/remove?cache=pages&id=", 170 | "removeTemplateCache": "/content/admin/remove?cache=templates&id=", 171 | "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder", 172 | "lookInContext": 1, 173 | "adminGroupID": 4, 174 | "betaServer": true}}], 175 | "servlet-mapping": { 176 | "cofaxCDS": "/", 177 | "cofaxEmail": "/cofaxutil/aemail/*", 178 | "cofaxAdmin": "/admin/*", 179 | "fileServlet": "/static/*", 180 | "cofaxTools": "/tools/*"}, 181 | "taglib": { 182 | "taglib-uri": "cofax.tld", 183 | "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}""", 184 | """ 185 | {"menu": { 186 | "header": "SVG Viewer", 187 | "items": [ 188 | {"id": "Open"}, 189 | {"id": "OpenNew", "label": "Open New"}, 190 | null, 191 | {"id": "ZoomIn", "label": "Zoom In"}, 192 | {"id": "ZoomOut", "label": "Zoom Out"}, 193 | {"id": "OriginalView", "label": "Original View"}, 194 | null, 195 | {"id": "Quality"}, 196 | {"id": "Pause"}, 197 | {"id": "Mute"}, 198 | null, 199 | {"id": "Find", "label": "Find..."}, 200 | {"id": "FindAgain", "label": "Find Again"}, 201 | {"id": "Copy"}, 202 | {"id": "CopyAgain", "label": "Copy Again"}, 203 | {"id": "CopySVG", "label": "Copy SVG"}, 204 | {"id": "ViewSVG", "label": "View SVG"}, 205 | {"id": "ViewSource", "label": "View Source"}, 206 | {"id": "SaveAs", "label": "Save As"}, 207 | null, 208 | {"id": "Help"}, 209 | {"id": "About", "label": "About Adobe CVG Viewer..."} 210 | ] 211 | }}""", 212 | """ 213 | { 214 | "k": "va\\"lue\\"" 215 | }""", 216 | """ 217 | { 218 | "k": "Stra\u00dfe" 219 | }""", 220 | """ 221 | { 222 | "k": "\\u81ea\\u7531" 223 | }""" 224 | }) 225 | void complexJsonDotOrgExamples(String json) throws IOException { 226 | var wheel = JsonWheel.read(json).inner; 227 | var jackson = JSON.std.mapFrom(json); 228 | assertThat(wheel).isEqualTo(jackson); 229 | } 230 | 231 | // Files taken from https://www.json.org/JSON_checker/test.zip. 232 | @ParameterizedTest(name = "pass{index}.json") 233 | @MethodSource("jsonDotOrgPassJsons") 234 | void jsonDotOrgPass(String json) throws IOException { 235 | var wheel = JsonWheel.read(json).inner; 236 | var jackson = JSON.std.anyFrom(json); 237 | assertThat(wheel).isEqualTo(jackson); 238 | } 239 | 240 | @ParameterizedTest(name = ParameterizedTest.INDEX_PLACEHOLDER) 241 | @ValueSource(strings = { 242 | """ 243 | { 244 | "k": "va\\"lue\\"" 245 | }""", 246 | """ 247 | { 248 | "k": "value\\\\" 249 | }""", 250 | """ 251 | { 252 | "k": "Stra\u00dfe" 253 | }""", 254 | """ 255 | { 256 | "k": "\\u81ea\\u7531" 257 | }""" 258 | }) 259 | void stringEncoding(String json) throws IOException { 260 | var wheel = JsonWheel.read(json).inner; 261 | var jackson = JSON.std.mapFrom(json); 262 | assertThat(wheel).isEqualTo(jackson); 263 | } 264 | } -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/JsonWheelTestMatrix.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.entry; 5 | import static org.assertj.core.api.InstanceOfAssertFactories.MAP; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import org.junit.jupiter.api.TestInstance; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.Arguments; 14 | import org.junit.jupiter.params.provider.MethodSource; 15 | 16 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 17 | abstract class JsonWheelTestMatrix { 18 | 19 | abstract List args(); 20 | 21 | @ParameterizedTest(name = "{index}: {0}") 22 | @MethodSource("args") 23 | void topLevel(Arg arg) { 24 | var actual = JsonWheel.read(arg.in); 25 | 26 | assertThat(actual.inner).isEqualTo(arg.expected); 27 | } 28 | 29 | @ParameterizedTest(name = "{index}: {0}") 30 | @MethodSource("args") 31 | void asSingleValueInObject(Arg arg) { 32 | var json = """ 33 | {"k": %s}""".formatted(arg.in); 34 | 35 | var actual = JsonWheel.read(json); 36 | 37 | assertThat(actual.inner) 38 | .asInstanceOf(MAP) 39 | .containsExactly( 40 | entry("k", arg.expected)); 41 | } 42 | 43 | @ParameterizedTest(name = "{index}: {0}") 44 | @MethodSource("args") 45 | void asValueInObjectStart(Arg arg) { 46 | var json = """ 47 | { 48 | "k1": %s, 49 | "k2": false, 50 | "k3": false 51 | }""".formatted(arg.in); 52 | 53 | var actual = JsonWheel.read(json); 54 | 55 | assertThat(actual.inner) 56 | .asInstanceOf(MAP) 57 | .containsExactly( 58 | entry("k1", arg.expected), 59 | entry("k2", false), 60 | entry("k3", false)); 61 | } 62 | 63 | @ParameterizedTest(name = "{index}: {0}") 64 | @MethodSource("args") 65 | void asValueInObjectMiddle(Arg arg) { 66 | var json = """ 67 | { 68 | "k1": false, 69 | "k2": %s, 70 | "k3": false 71 | }""".formatted(arg.in); 72 | 73 | var actual = JsonWheel.read(json); 74 | 75 | assertThat(actual.inner) 76 | .asInstanceOf(MAP) 77 | .containsExactly( 78 | entry("k1", false), 79 | entry("k2", arg.expected), 80 | entry("k3", false)); 81 | } 82 | 83 | @ParameterizedTest(name = "{index}: {0}") 84 | @MethodSource("args") 85 | void asValueInObjectEnd(Arg arg) { 86 | var json = """ 87 | { 88 | "k1": false, 89 | "k2": false, 90 | "k3": %s 91 | }""".formatted(arg.in); 92 | 93 | var actual = JsonWheel.read(json); 94 | 95 | assertThat(actual.inner) 96 | .asInstanceOf(MAP) 97 | .containsExactly( 98 | entry("k1", false), 99 | entry("k2", false), 100 | entry("k3", arg.expected)); 101 | } 102 | 103 | @ParameterizedTest(name = "{index}: {0}") 104 | @MethodSource("args") 105 | void asSingleValueInArray(Arg arg) { 106 | var json = """ 107 | [ 108 | %s 109 | ]""".formatted(arg.in); 110 | 111 | var actual = JsonWheel.read(json); 112 | 113 | assertThat(actual.inner).isEqualTo(Collections.singletonList(arg.expected)); 114 | } 115 | 116 | @ParameterizedTest(name = "{index}: {0}") 117 | @MethodSource("args") 118 | void asValueInArrayStart(Arg arg) { 119 | var json = """ 120 | [ 121 | %s, 122 | null, 123 | null 124 | ]""".formatted(arg.in); 125 | 126 | var actual = JsonWheel.read(json); 127 | 128 | assertThat(actual.inner).isEqualTo(Arrays.asList(arg.expected, null, null)); 129 | } 130 | 131 | @ParameterizedTest(name = "{index}: {0}") 132 | @MethodSource("args") 133 | void asValueInArrayEnd(Arg arg) { 134 | var json = """ 135 | [ 136 | null, 137 | null, 138 | %s 139 | ]""".formatted(arg.in); 140 | 141 | var actual = JsonWheel.read(json); 142 | 143 | assertThat(actual.inner).isEqualTo(Arrays.asList(null, null, arg.expected)); 144 | } 145 | 146 | @ParameterizedTest(name = "{index}: {0}") 147 | @MethodSource("args") 148 | void asValueInArrayMiddle(Arg arg) { 149 | var json = """ 150 | [ 151 | null, 152 | %s, 153 | null 154 | ]""".formatted(arg.in); 155 | 156 | var actual = JsonWheel.read(json); 157 | 158 | assertThat(actual.inner).isEqualTo(Arrays.asList(null, arg.expected, null)); 159 | } 160 | 161 | record Arg(String in, Object expected) { 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/NullTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.params.provider.Arguments; 9 | 10 | class NullTest extends JsonWheelTestMatrix { 11 | 12 | @Override 13 | List args() { 14 | return List.of( 15 | arguments(named("null", new Arg("null", null)))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/NumberTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.math.BigDecimal; 7 | import java.math.BigInteger; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.params.provider.Arguments; 11 | 12 | class NumberTest extends JsonWheelTestMatrix { 13 | 14 | @Override 15 | List args() { 16 | return List.of( 17 | arguments(named("positive integer", new Arg("1", 1))), 18 | arguments(named("negative integer", new Arg("-1", -1))), 19 | arguments(named("positive integer boundary", new Arg("2147483647", Integer.MAX_VALUE))), 20 | arguments(named("negative integer boundary", new Arg("-2147483648", Integer.MIN_VALUE))), 21 | arguments(named("positive double precision integer", new Arg("2147483648", Integer.MAX_VALUE + 1L))), 22 | arguments(named("negative double precision integer", new Arg("-2147483649", Integer.MIN_VALUE - 1L))), 23 | arguments(named("positive double precision integer boundary", new Arg("9223372036854775807", Long.MAX_VALUE))), 24 | arguments(named("negative double precision integer boundary", new Arg("-9223372036854775808", Long.MIN_VALUE))), 25 | arguments(named("positive arbitrary precision integer", new Arg("9223372036854775808", new BigInteger("9223372036854775808")))), // Long.MAX_VALUE + 1 26 | arguments(named("negative arbitrary precision integer", new Arg("-9223372036854775809", new BigInteger("-9223372036854775809")))), // Long.MIN_VALUE - 1 27 | arguments(named("positive decimal", new Arg("1.2", 1.2d))), 28 | arguments(named("negative decimal", new Arg("-1.2", -1.2d))), 29 | arguments(named("positive decimal boundary high", new Arg("1.7976931348623157E308", Double.MAX_VALUE))), 30 | arguments(named("positive decimal boundary low", new Arg("4.9E-324", Double.MIN_VALUE))), 31 | arguments(named("positive arbitrary precision decimal high", new Arg("1.7976931348623157E309", new BigDecimal("1.7976931348623157E309")))), // Double.MAX_VALUE * 10 32 | arguments(named("positive arbitrary precision decimal low", new Arg("4.9E-325", new BigDecimal("4.9E-325")))), // Double.MIN_VALUE / 10 33 | arguments(named("negative arbitrary precision decimal high", new Arg("-1.7976931348623157E309", new BigDecimal("-1.7976931348623157E309")))), // -Double.MAX_VALUE * 10 34 | arguments(named("negative arbitrary precision decimal low", new Arg("-4.9E-325", new BigDecimal("-4.9E-325")))), // -Double.MIN_VALUE / 10 35 | arguments(named("scientific notation exponent capitalized", new Arg("5E2", 500d))), 36 | arguments(named("scientific notation exponent non-capitalized", new Arg("5e2", 500d))), 37 | arguments(named("scientific notation exponent with positive sign", new Arg("5e+2", 500d))), 38 | arguments(named("scientific notation exponent with negative sign", new Arg("5e-2", 0.05d))), 39 | arguments(named("positive integer scientific notation positive exponent", new Arg("5e2", 500d))), 40 | arguments(named("negative integer scientific notation positive exponent", new Arg("-5e2", -500d))), 41 | arguments(named("positive integer scientific notation negative exponent", new Arg("5e-2", 0.05d))), 42 | arguments(named("negative integer scientific notation negative exponent", new Arg("-5e-2", -0.05d))), 43 | arguments(named("positive decimal scientific notation positive exponent", new Arg("5.1e2", 510d))), 44 | arguments(named("negative decimal scientific notation positive exponent", new Arg("-5.1e2", -510d))), 45 | arguments(named("positive decimal scientific notation negative exponent", new Arg("5.1e-2", 0.051d))), 46 | arguments(named("negative decimal scientific notation negative exponent", new Arg("-5.1e-2", -0.051d)))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/ObjectTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import org.junit.jupiter.params.provider.Arguments; 12 | 13 | class ObjectTest extends JsonWheelTestMatrix { 14 | 15 | @Override 16 | List args() { 17 | return List.of( 18 | arguments(named("empty object", new Arg("{}", Collections.emptyMap()))), 19 | arguments(named("object single kv string", new Arg("{\"foo\": \"bar\"}", Map.of("foo", "bar")))), 20 | arguments(named("object single kv number", new Arg("{\"foo\": 1.1}", Map.of("foo", 1.1d)))), 21 | arguments(named("object single kv boolean", new Arg("{\"foo\": true}", Map.of("foo", true)))), 22 | arguments(named("object single kv null", new Arg("{\"foo\": null}", new HashMap<>() { 23 | { 24 | put("foo", null); 25 | } 26 | }))), 27 | arguments(named("object single kv array", new Arg("{\"foo\": [1.1]}", Map.of("foo", List.of(1.1d))))), 28 | arguments(named("object single kv object", new Arg("{\"foo\": {\"bar\": 1.1}}", Map.of("foo", Map.of("bar", 1.1d))))), 29 | arguments( 30 | named("object multiple kv", new Arg("{\"foo\": \"bar\", \"baz\": \"qux\", \"blerg\": 0.5}", Map.of("foo", "bar", "baz", "qux", "blerg", 0.5d))))); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/StringTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.junit.jupiter.api.Named.named; 4 | import static org.junit.jupiter.params.provider.Arguments.arguments; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.params.provider.Arguments; 9 | 10 | class StringTest extends JsonWheelTestMatrix { 11 | 12 | @Override 13 | List args() { 14 | return List.of( 15 | arguments(named("simple string", new Arg("\"hello, world\"", "hello, world"))), 16 | arguments(named("empty string", new Arg("\"\"", ""))), 17 | arguments(named("whitespace", new Arg("\" \"", " "))), 18 | arguments(named("quotes", new Arg("\"quo\\\"tes\\\"\"", "quo\"tes\""))), 19 | arguments(named("just quote", new Arg("\"\\\"\"", "\""))), 20 | arguments(named("colon", new Arg("\"co:lon\"", "co:lon"))), 21 | arguments(named("comma", new Arg("\"com,ma\"", "com,ma"))), 22 | arguments(named("opening brace", new Arg("\"bra{ce\"", "bra{ce"))), 23 | arguments(named("closing brace", new Arg("\"bra}ce\"", "bra}ce"))), 24 | arguments(named("opening bracket", new Arg("\"bra[cket\"", "bra[cket"))), 25 | arguments(named("closing bracket", new Arg("\"bra]cket\"", "bra]cket"))), 26 | arguments(named("solidus", new Arg("\"soli\\/dus\"", "soli/dus"))), 27 | arguments(named("reverse solidus", new Arg("\"reverse soli\\\\dus\"", "reverse soli\\dus"))), 28 | arguments(named("just reverse solidus", new Arg("\"\\\\\"", "\\"))), 29 | arguments(named("horizontal tab", new Arg("\"horizontal\\ttab\"", "horizontal\ttab"))), 30 | arguments(named("carriage return", new Arg("\"carriage\\rreturn\"", "carriage\rreturn"))), 31 | arguments(named("line feed", new Arg("\"line\\nfeed\"", "line\nfeed"))), 32 | arguments(named("form feed", new Arg("\"form\\ffeed\"", "form\ffeed"))), 33 | arguments(named("backspace", new Arg("\"back\\bspace\"", "back\bspace"))), 34 | arguments(named("all controls", new Arg("\"\\b\\f\\n\\r\\t\"", "\b\f\n\r\t"))), 35 | arguments(named("non-ASCII 1", new Arg("\"Straße\"", "Straße"))), 36 | arguments(named("non-ASCII 2", new Arg("\"自由\"", "自由"))), 37 | arguments(named("non-ASCII 3", new Arg("\"🧪\"", "🧪"))), 38 | arguments(named("escaped non-ASCII 1", new Arg("\"Stra\\u00dfe\"", "Straße"))), 39 | arguments(named("escaped non-ASCII 2", new Arg("\"\\u81ea\\u7531\"", "自由"))), 40 | arguments(named("escaped non-ASCII 3", new Arg("\"\\uD83E\\uDDEA\"", "🧪")))); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/romanboehm/jsonwheel/WheelNodeTest.java: -------------------------------------------------------------------------------- 1 | package com.romanboehm.jsonwheel; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.ValueSource; 9 | 10 | import com.romanboehm.jsonwheel.JsonWheel.JsonWheelException; 11 | 12 | class WheelNodeTest { 13 | 14 | @Test 15 | void access1() { 16 | var json = """ 17 | { 18 | "foo": { 19 | "bar": [ 20 | 1, 21 | 2, 22 | 3 23 | ], 24 | "baz": "qux" 25 | } 26 | }"""; 27 | 28 | var node = JsonWheel.read(json); 29 | 30 | var bar = node.get("foo").get("bar"); 31 | assertThat(bar.elements()).extracting(wn -> wn.val(Integer.class)).containsExactly( 32 | 1, 33 | 2, 34 | 3); 35 | 36 | var baz = node.get("foo").get("baz"); 37 | assertThat(baz.val(String.class)).isEqualTo("qux"); 38 | } 39 | 40 | @Test 41 | void access2() { 42 | var json = """ 43 | { 44 | "foo": { 45 | "bar": [ 46 | { 47 | "k1": "v1" 48 | }, 49 | { 50 | "k2": "v2" 51 | } 52 | ] 53 | } 54 | }"""; 55 | 56 | var node = JsonWheel.read(json); 57 | 58 | var bar = node.get("foo").get("bar").elements(); 59 | var k1 = bar.get(0).get("k1"); 60 | assertThat(k1.val(String.class)).isEqualTo("v1"); 61 | } 62 | 63 | @Test 64 | void wrongAccess() { 65 | var json = """ 66 | { 67 | "foo": "bar" 68 | }"""; 69 | 70 | var node = JsonWheel.read(json); 71 | 72 | assertThatThrownBy(() -> node.get("foo").val(Integer.class)) 73 | .isInstanceOf(RuntimeException.class); 74 | } 75 | 76 | @Test 77 | void accessNull() { 78 | var json = """ 79 | { 80 | "foo": null 81 | }"""; 82 | 83 | var node = JsonWheel.read(json); 84 | assertThat(node.get("foo").val(String.class)).isNull(); 85 | } 86 | 87 | @Test 88 | void accessTopLevelNull() { 89 | var json = "null"; 90 | var node = JsonWheel.read(json); 91 | assertThat(node.val(Object.class)).isNull(); 92 | } 93 | 94 | @Test 95 | void accessTopLevelTrue() { 96 | var json = "true"; 97 | var node = JsonWheel.read(json); 98 | assertThat(node.val(Boolean.class)).isTrue(); 99 | } 100 | 101 | @Test 102 | void accessTopLevelFalse() { 103 | var json = "false"; 104 | var node = JsonWheel.read(json); 105 | assertThat(node.val(Boolean.class)).isFalse(); 106 | } 107 | 108 | @ParameterizedTest 109 | @ValueSource(strings = { "notnull", "fakefalse", "thetrue" }) 110 | void accessInvalidLiteralValue(String input) { 111 | var json = """ 112 | { 113 | "foo": %s, 114 | "bar": "ohoh" 115 | }""".formatted(input); 116 | 117 | assertThatThrownBy(() -> JsonWheel.read(json)) 118 | .isInstanceOf(JsonWheelException.class); 119 | } 120 | 121 | @Test 122 | void accessBooleanValue() { 123 | var json = """ 124 | { 125 | "foo": true, 126 | "bar": "ohoh" 127 | }"""; 128 | 129 | var node = JsonWheel.read(json); 130 | var foo = node.get("foo"); 131 | var bar = node.get("bar"); 132 | assertThat(foo.val(Boolean.class)).isTrue(); 133 | assertThat(bar.val(String.class)).isEqualTo("ohoh"); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/resources/jsondotorg/pass/pass1.json: -------------------------------------------------------------------------------- 1 | [ 2 | "JSON Test Pattern pass1", 3 | {"object with 1 member":["array with 1 element"]}, 4 | {}, 5 | [], 6 | -42, 7 | true, 8 | false, 9 | null, 10 | { 11 | "integer": 1234567890, 12 | "real": -9876.543210, 13 | "e": 0.123456789e-12, 14 | "E": 1.234567890E+34, 15 | "": 23456789012E66, 16 | "zero": 0, 17 | "one": 1, 18 | "space": " ", 19 | "quote": "\"", 20 | "backslash": "\\", 21 | "controls": "\b\f\n\r\t", 22 | "slash": "/ & \/", 23 | "alpha": "abcdefghijklmnopqrstuvwyz", 24 | "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", 25 | "digit": "0123456789", 26 | "0123456789": "digit", 27 | "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", 28 | "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", 29 | "true": true, 30 | "false": false, 31 | "null": null, 32 | "array":[ ], 33 | "object":{ }, 34 | "address": "50 St. James Street", 35 | "url": "http://www.JSON.org/", 36 | "comment": "// /* */": " ", 38 | " s p a c e d " :[1,2 , 3 39 | 40 | , 41 | 42 | 4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], 43 | "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", 44 | "quotes": "" \u0022 %22 0x22 034 "", 45 | "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" 46 | : "A key can be any string" 47 | }, 48 | 0.5 ,98.6 49 | , 50 | 99.44 51 | , 52 | 53 | 1066, 54 | 1e1, 55 | 0.1e1, 56 | 1e-1, 57 | 1e00,2e+00,2e-00 58 | ,"rosebud"] -------------------------------------------------------------------------------- /src/test/resources/jsondotorg/pass/pass2.json: -------------------------------------------------------------------------------- 1 | [[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] -------------------------------------------------------------------------------- /src/test/resources/jsondotorg/pass/pass3.json: -------------------------------------------------------------------------------- 1 | { 2 | "JSON Test Pattern pass3": { 3 | "The outermost value": "must be an object or array.", 4 | "In this test": "It is an object." 5 | } 6 | } 7 | --------------------------------------------------------------------------------