├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── changelog.md ├── pom.xml ├── pom └── pom.xml ├── samples ├── json-interop.md ├── main-gson │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── Main.java ├── main-jackson │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── Main.java └── main │ ├── pom.xml │ └── src │ └── main │ └── java │ ├── Main.java │ └── MainJson.java ├── src ├── main │ └── java │ │ ├── com │ │ └── dashjoin │ │ │ └── jsonata │ │ │ ├── Functions.java │ │ │ ├── JException.java │ │ │ ├── Jsonata.java │ │ │ ├── Parser.java │ │ │ ├── Timebox.java │ │ │ ├── Tokenizer.java │ │ │ ├── Utils.java │ │ │ ├── json │ │ │ ├── Json.java │ │ │ ├── JsonHandler.java │ │ │ ├── JsonParser.java │ │ │ ├── Location.java │ │ │ └── ParseException.java │ │ │ └── utils │ │ │ ├── Constants.java │ │ │ ├── DateTimeUtils.java │ │ │ └── Signature.java │ │ └── module-info.java └── test │ └── java │ └── com │ └── dashjoin │ └── jsonata │ ├── .gitignore │ ├── ArrayTest.java │ ├── CustomFunctionTest.java │ ├── DateTimeTest.java │ ├── Generate.java │ ├── JsonataTest.java │ ├── NullSafetyTest.java │ ├── NumberTest.java │ ├── ParseIntegerTest.java │ ├── SerializationTest.java │ ├── SignatureTest.java │ ├── StringTest.java │ ├── ThreadTest.java │ └── TypesTest.java └── test └── test-overrides.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | cache: maven 25 | 26 | - name: Build with Maven 27 | run: | 28 | mvn package -DskipTests -Dmaven.javadoc.skip=true 29 | mvn compile exec:java -Dexec.classpathScope=test -Dexec.mainClass=com.dashjoin.jsonata.Generate 30 | mvn package -DskipTests -Dmaven.javadoc.skip=true 31 | mvn test 32 | 33 | - name: Publish Test Results 34 | uses: EnricoMi/publish-unit-test-result-action@v2 35 | if: always() 36 | with: 37 | files: | 38 | target/surefire-reports/**/*.xml 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .DS_Store 3 | .vscode 4 | .java-version 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jsonata"] 2 | path = jsonata 3 | url = https://github.com/jsonata-js/jsonata 4 | branch = master 5 | -------------------------------------------------------------------------------- /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 | # jsonata-java is the JSONata Java reference port 2 | JSONata reference ported to Java 3 | 4 | This is a 1:1 Java port of the [JSONata reference implementation](https://github.com/jsonata-js/jsonata) 5 | 6 | ## Features 7 | * [100% JSONata feature compatibility](https://github.com/dashjoin/jsonata-java/actions/runs/5717119540/job/15490217787) 8 | - All JSONata language features supported 9 | - [100% reference test coverage](https://github.com/dashjoin/jsonata-java/actions/runs/5717119540/job/15490217787) [with well justified exceptions](https://github.com/dashjoin/jsonata-java/blob/main/test/test-overrides.json) 10 | * Error messages matching the reference 11 | - Even stack traces are comparable 12 | * Zero dependency and small 13 | - Only 160 kB total size 14 | * [JSON parser agnostic](https://github.com/dashjoin/jsonata-java/blob/main/samples/json-interop.md) 15 | - use with Jackson, GSon, ... 16 | - comes with integrated vanilla parser 17 | * Performance optimized & thread safe 18 | - [Native jsonata command line tool](https://github.com/dashjoin/jsonata-cli) 19 | * Enterprise support 20 | - [Premium support available from the original developers](https://dashjoin.com) 21 | 22 | ## Quick Start 23 | 24 | Add the dependency in pom.xml: 25 | ```xml 26 | 27 | com.dashjoin 28 | jsonata 29 | 0.9.8 30 | 31 | ``` 32 | Here is the [release change log](changelog.md) 33 | 34 | Main.java program: 35 | ```Java 36 | import java.util.List; import java.util.Map; 37 | import static com.dashjoin.jsonata.Jsonata.jsonata; 38 | 39 | public class Main { 40 | public static void main(String[] args) { 41 | 42 | var data = Map.of("example", 43 | List.of( 44 | Map.of("value", 4), 45 | Map.of("value", 7), 46 | Map.of("value", 13) 47 | ) 48 | ); 49 | 50 | var expression = jsonata("$sum(example.value)"); 51 | var result = expression.evaluate(data); // returns 24 52 | System.out.println(result); 53 | } 54 | } 55 | ``` 56 | 57 | ### Custom Functions 58 | 59 | You can define custom functions and declare variables via a JSONata frame or 60 | the registerFunction method: 61 | 62 | ```Java 63 | var expression = jsonata("$sum(example.value) + $sin($PI/2)"); 64 | 65 | // Default JSONata has no $sin function and no $PI, so define it 66 | var env = expression.createFrame(); 67 | env.bind("sin", (Number n) -> Math.sin( n.doubleValue() ) ); 68 | env.bind("PI", Math.PI); 69 | 70 | var result = expression.evaluate(data, env); // returns 25 71 | ``` 72 | 73 | For more examples, please refer to this [test case](https://github.com/dashjoin/jsonata-java/blob/main/src/test/java/com/dashjoin/jsonata/CustomFunctionTest.java). 74 | 75 | ## History 76 | We needed a high performance and 100% compatible engine for the ETL and data transformations of the [Dashjoin Low Code platform](https://github.com/dashjoin/platform). Being a JSON full stack based on Quarkus/Java, JSONata was a very good fit and is even more today. 77 | 78 | In the beginning we used the original Java port, but quickly got lots of issues due to unsupported features and errors that we could not reproduce easily. 79 | The next solution which was running quite well and stable was to use GraalVM's Javascript engine to run the jsonata-js reference implementation in process of the Java backend. This works OK, but there are performance compromises, especially when there are many switches between the Javascript and the Java context (as is the case with ETL and data transformations). 80 | 81 | ## Design 82 | Working with Java since its inception in 1996, we made an experiment to see what it would take to port the existing reference Javascript into working and performant Java. This experiment went so well that we decided to work on a 100% port of the JSONata reference engine - the result which you can see in this repository. 83 | 84 | ### No generic Java types 85 | To get a 1:1 readable port, we decided to not use any generic types (yes, so basically this looks like 20 years old Java code...) - 86 | but it has many advantages in this specific case: 87 | * Java code nearly looks the same as Javascript 88 | * Patches and fixes coming into the Javascript reference are easily portable 89 | 90 | We lose the type safety and compile time checks Java generics introduced, but since the job is to port Javascript code, we are in an 'un-typed' world anyway. 91 | 92 | ### No JSON wrapper library 93 | To get as near as possible to the Javascript syntax, decision was made to use 94 | * java.util.Map as Javascript object 95 | - which in turn represents a JSON object 96 | * java.util.List as Javascript array 97 | - which in turn represents JSON lists/JSONata sequences 98 | * String, numbers, and boolean can be used as well 99 | * [This test case](https://github.com/dashjoin/jsonata-java/blob/main/src/test/java/com/dashjoin/jsonata/TypesTest.java) documents which types are legal and how you can use a JSON library like Jackson to convert Pojos and other types 100 | 101 | No JSON lib like Jackson is being used. This has advantages, but needs careful design w.r.t. how the logic is being ported. 102 | ### The big 'null vs undefined' question 103 | Porting Javascript code gets ambiguous as soon as there is a boolean expression that might depend on null and/or undefined. 104 | In Java there are basically these solutions: 105 | * use a Holder class that can disambiguate the null/undefined cases 106 | * Java null means null, use a special value/object for UNDEFINED_VALUE 107 | * Java null means undefined, use a special value/object for NULL_VALUE 108 | 109 | JSON libs will usually use a Holder variant (implemented in some JSONValue implementation). 110 | After review, it turned out that we can stay as near as possible to the original code structure (with as little special code as possible) by using the 3rd variant. 111 | 112 | ### Numeric Values 113 | 114 | Compared to Java, Javascript handles numbers quite differently. 115 | Whenever a computation is done, the engine tries to "fit" the result into an int, long, or double (see com.dashjoin.jsonata.Utils.convertNumber(Number)). 116 | This [test case](https://github.com/dashjoin/jsonata-java/blob/main/src/test/java/com/dashjoin/jsonata/NumberTest.java) 117 | shows this behavior. 118 | 119 | ## Performance 120 | We conducted some experiments to measure performance, but it's not an 'overall benchmark' yet. Your mileage may vary... 121 | 122 | |Expression| jsonata-js | JSONata4Java | jsonata-java | speedup factor | 123 | |----------|------------|--------------|---|---| 124 | | function-sift 4 | 26.1 / 109.6 | 36.1 / 144.8 | 140.8 / 348.2 | 3.9 / 2.3 | 125 | | hof-map 0 | 16.4 / 62.2 | 17.7 / 352.8 | 66.2 / 295.2 | 3.7 / 0.8 | 126 | | hof-zip 2 | 15.4 / 59.2 | 16.7 / exception | 64.2 / 312.6 | 3.8 / ? | 127 | | hof-zip-map 0 | 16.0 / 57.3 | 12.6 / 227.7 (wrong) | 58.3 / 323.6 | 4.6 / 1.4 | 128 | | partial-application 2 | 26.1 / 29.1 | parser error | 162.3 / 133.4 | ? / ? | 129 | | [1..500].($*$)~>$sum | 24.4 / 1.8 | 159.6 / exception | 286.4 / 9.0 | 1.8 / ? | 130 | 131 | - Expression denotes the test suite name and case. 132 | - First figure = parse operations, second figure = evaluate operations. 133 | - Performance measured in kiloOps/s (thousands of operations per second), higher means faster. 134 | - Speedup factor compared to JSONata4Java (2.0 means "twice as fast"). 135 | 136 | ## Developers: getting started 137 | 138 | The project uses the repository of the reference implementation as a submodule. 139 | This allows referencing the current version of the unit tests. 140 | To clone this repository, run: 141 | 142 | ``` 143 | git clone --recurse-submodules https://github.com/dashjoin/jsonata-java 144 | ``` 145 | 146 | To compile, generate / run the unit tests, and create the jar file, run: 147 | 148 | ``` 149 | mvn compile exec:java -Dexec.classpathScope=test -Dexec.mainClass=com.dashjoin.jsonata.Generate 150 | mvn install 151 | ``` 152 | 153 | ## Contribute 154 | 155 | We welcome contributions. If you are interested in contributing to Dashjoin, let us know! 156 | You'll get to know an open-minded and motivated team working together to build the next generation platform. 157 | 158 | * [Join our Slack](https://join.slack.com/t/dashjoin/shared_invite/zt-1274qbzq9-mwxBq4WwSTJsITjrvYV4pA) and say hello 159 | * [Follow us](https://twitter.com/dashjoin) on Twitter 160 | * [Submit](https://github.com/dashjoin/jsonata-java/issues) your ideas by opening an issue with the enhancement label 161 | * [Help out](https://github.com/dashjoin/jsonata-java/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) by fixing "a good first issue" 162 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Release change log 2 | 3 | ## 0.9.8 - 2024/10/14 4 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/7?closed=1) 5 | 6 | ## 0.9.7 - 2024/06/11 7 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/6?closed=1) 8 | 9 | ## 0.9.6 - 2024/04/05 10 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/5?closed=1) 11 | 12 | ## 0.9.5 - 2023/11/20 13 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/4?closed=1) 14 | 15 | ## 0.9.4 - 2023/10/31 16 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/3?closed=1) 17 | 18 | ## 0.9.3 - 2023/10/05 19 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/2?closed=1) 20 | 21 | ## 0.9.2 - 2023/08/02 22 | - [List of enhancements and fixes](https://github.com/dashjoin/jsonata-java/milestone/1?closed=1) 23 | 24 | ## 0.9.1 - 2023/07/30 25 | - Parser NPE error fixed 26 | 27 | ## 0.9.0 - 2023/07/30 28 | - Initial release 29 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.dashjoin 5 | jsonata 6 | 0.9.8 7 | 8 | 9 | com.dashjoin 10 | jsonata-parent 11 | pom 12 | 0.9.2024-10 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pom/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.dashjoin 5 | jsonata-parent 6 | 0.9.2024-10 7 | pom 8 | 9 | UTF-8 10 | 11 11 | 20 12 | 13 | 14 | ${project.groupId}:${project.artifactId} 15 | JSONata Java reference port 16 | https://github.com/dashjoin/jsonata-java 17 | 18 | 19 | 20 | Apache License, Version 2.0 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | 24 | 25 | 26 | 27 | Dashjoin Dev Team 28 | info@dashjoin.com 29 | Dashjoin GmbH 30 | https://dashjoin.com 31 | 32 | 33 | 34 | https://github.com/dashjoin/jsonata-java.git 35 | https://github.com/dashjoin/jsonata-java.git 36 | https://github.com/dashjoin/jsonata-java 37 | 38 | 39 | 40 | 41 | ossrh 42 | https://s01.oss.sonatype.org/content/repositories/snapshots 43 | 44 | 45 | ossrh 46 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 47 | 48 | 49 | 50 | 51 | 52 | org.junit.jupiter 53 | junit-jupiter 54 | 5.11.2 55 | test 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-databind 60 | 2.18.0 61 | test 62 | 63 | 64 | commons-io 65 | commons-io 66 | 2.17.0 67 | test 68 | 69 | 70 | org.apache.commons 71 | commons-text 72 | 1.12.0 73 | test 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-surefire-plugin 83 | 3.5.1 84 | 85 | 86 | maven-resources-plugin 87 | 3.3.1 88 | 89 | 90 | 91 | 92 | 93 | org.codehaus.mojo 94 | exec-maven-plugin 95 | 3.4.1 96 | 97 | 98 | java 99 | 100 | java 101 | 102 | 103 | ${mainClass} 104 | 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-compiler-plugin 112 | 3.13.0 113 | 114 | ${maven.compiler.source} 115 | ${maven.compiler.source} 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-jar-plugin 122 | 3.4.2 123 | 124 | 125 | 126 | true 127 | ${mainClass} 128 | 129 | 130 | 131 | 132 | 133 | 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-source-plugin 161 | 3.3.1 162 | 163 | 164 | attach-sources 165 | 166 | jar-no-fork 167 | 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-javadoc-plugin 174 | 3.10.1 175 | 176 | 177 | false 178 | false 179 | 180 | 181 | 182 | 183 | attach-javadocs 184 | 185 | jar 186 | 187 | 188 | 189 | 190 | 191 | org.apache.maven.plugins 192 | maven-gpg-plugin 193 | 3.2.7 194 | 195 | 196 | sign-artifacts 197 | verify 198 | 199 | sign 200 | 201 | 202 | ${gpg.keyname} 203 | ${gpg.keyname} 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /samples/json-interop.md: -------------------------------------------------------------------------------- 1 | # Interoperability with JSON libraries 2 | 3 | jsonata-java is agnostic and works with any JSON library. 4 | 5 | # Internal JSON representation 6 | 7 | Consider the following JSON data: 8 | ```json 9 | { 10 | "example": [ 11 | { 12 | "value": 4 13 | }, 14 | { 15 | "value": 7 16 | }, 17 | { 18 | "value": 13 19 | } 20 | ] 21 | } 22 | ``` 23 | 24 | The internal JSON representation is based on Map for JSON objects and List for JSON arrays/collections. 25 | 26 | This is the format jsonata-java operates on when evaluating expressions. 27 | This format has to be used when JSON is fed as input into the evaluation function. 28 | 29 | 30 | ```Java 31 | var data = Map.of("example", 32 | List.of( 33 | Map.of("value", 4), 34 | Map.of("value", 7), 35 | Map.of("value", 13) 36 | ) 37 | ); 38 | ``` 39 | 40 | # Built-in JSON parsing and stringifying 41 | 42 | jsonata-java comes with built-in support for decoding and stringifying JSON: 43 | 44 | ```Java 45 | import com.dashjoin.jsonata.json.Json; 46 | 47 | // jsonInput is a String or Reader: 48 | var data = Json.parseJson(jsonInput); 49 | 50 | var expression = jsonata("$sum(example.value)"); 51 | var result = expression.evaluate(data); // returns 24 52 | ``` 53 | 54 | To output JSON, you can use the ```Functions.string``` function: 55 | ```Java 56 | boolean beautify = true; 57 | String toString = Functions.string(json, beautify); 58 | ``` 59 | 60 | See [these complete samples](https://github.com/dashjoin/jsonata-java/tree/main/samples/main) for both internal format and JSON parsing cases. 61 | 62 | # Jackson Databind 63 | 64 | Jackson integration is straightforward, just import the JSON as ```Object.class``` to feed it to jsonata-java: 65 | 66 | ```Java 67 | var data = new ObjectMapper().readValue(json, Object.class); 68 | 69 | var expression = jsonata("$sum(example.value)"); 70 | var result = expression.evaluate(data); // returns 24 71 | ``` 72 | 73 | See [this complete sample](https://github.com/dashjoin/jsonata-java/tree/main/samples/main-jackson). 74 | 75 | # Google GSON 76 | 77 | GSON integration is straightforward, just import the JSON as ```Object.class``` to feed it to jsonata-java: 78 | 79 | ```Java 80 | Gson gson = new Gson(); 81 | var data = gson.fromJson(json, Object.class); 82 | 83 | var expression = jsonata("$sum(example.value)"); 84 | var result = expression.evaluate(data); // returns 24 85 | ``` 86 | 87 | See [this complete sample](https://github.com/dashjoin/jsonata-java/tree/main/samples/main-gson). 88 | -------------------------------------------------------------------------------- /samples/main-gson/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | 7 | 1.0 8 | UTF-8 9 | 11 10 | 11 11 | Main 12 | 13 | 14 | com.dashjoin 15 | samples-main-gson 16 | ${revision} 17 | 18 | 19 | 20 | com.dashjoin 21 | jsonata 22 | 0.9.8 23 | 24 | 25 | 26 | com.google.code.gson 27 | gson 28 | 2.11.0 29 | compile 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-assembly-plugin 38 | 39 | 40 | package 41 | 42 | single 43 | 44 | 45 | 46 | 47 | 48 | 49 | true 50 | ${mainClass} 51 | 52 | 53 | 54 | jar-with-dependencies 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /samples/main-gson/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import java.util.Map; import java.util.List; 2 | import static com.dashjoin.jsonata.Jsonata.jsonata; 3 | import com.google.gson.Gson; 4 | 5 | public class Main { 6 | 7 | /** 8 | * Interoperability with GSON library. 9 | */ 10 | public static void main(String[] args) { 11 | 12 | String json = "{\n" + // 13 | " \"example\": [\n" + // 14 | " {\n" + // 15 | " \"value\": 4\n" + // 16 | " },\n" + // 17 | " {\n" + // 18 | " \"value\": 7\n" + // 19 | " },\n" + // 20 | " {\n" + // 21 | " \"value\": 13\n" + // 22 | " }\n" + // 23 | " ]\n" + // 24 | "}"; 25 | 26 | Gson gson = new Gson(); 27 | var data = gson.fromJson(json, Object.class); 28 | 29 | var expression = jsonata("$sum(example.value)"); 30 | var result = expression.evaluate(data); // returns 24 31 | System.out.println(result); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/main-jackson/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | 7 | 1.0 8 | UTF-8 9 | 11 10 | 11 11 | Main 12 | 13 | 14 | com.dashjoin 15 | samples-main-jackson 16 | ${revision} 17 | 18 | 19 | 20 | com.dashjoin 21 | jsonata 22 | 0.9.8 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-databind 27 | 2.18.0 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.apache.maven.plugins 35 | maven-assembly-plugin 36 | 37 | 38 | package 39 | 40 | single 41 | 42 | 43 | 44 | 45 | 46 | 47 | true 48 | ${mainClass} 49 | 50 | 51 | 52 | jar-with-dependencies 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /samples/main-jackson/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import java.util.Map; import java.util.List; 2 | import static com.dashjoin.jsonata.Jsonata.jsonata; 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | public class Main { 6 | 7 | /** 8 | * Interoperability with Jackson Databind library. 9 | */ 10 | public static void main(String[] args) throws Throwable { 11 | 12 | String json = "{\n" + // 13 | " \"example\": [\n" + // 14 | " {\n" + // 15 | " \"value\": 4\n" + // 16 | " },\n" + // 17 | " {\n" + // 18 | " \"value\": 7\n" + // 19 | " },\n" + // 20 | " {\n" + // 21 | " \"value\": 13\n" + // 22 | " }\n" + // 23 | " ]\n" + // 24 | "}"; 25 | 26 | var data = new ObjectMapper().readValue(json, Object.class); 27 | 28 | var expression = jsonata("$sum(example.value)"); 29 | var result = expression.evaluate(data); // returns 24 30 | System.out.println(result); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/main/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | 7 | 1.0 8 | UTF-8 9 | 11 10 | 11 11 | Main 12 | 13 | 14 | com.dashjoin 15 | samples-main 16 | ${revision} 17 | 18 | 19 | 20 | com.dashjoin 21 | jsonata 22 | 0.9.8 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-assembly-plugin 31 | 32 | 33 | package 34 | 35 | single 36 | 37 | 38 | 39 | 40 | 41 | 42 | true 43 | ${mainClass} 44 | 45 | 46 | 47 | jar-with-dependencies 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /samples/main/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import java.util.Map; import java.util.List; 2 | import static com.dashjoin.jsonata.Jsonata.jsonata; 3 | 4 | public class Main { 5 | 6 | /** 7 | * Feed JSON in internal representation format. 8 | */ 9 | public static void main(String[] args) { 10 | 11 | var data = Map.of("example", 12 | List.of( 13 | Map.of("value", 4), 14 | Map.of("value", 7), 15 | Map.of("value", 13) 16 | ) 17 | ); 18 | 19 | var expression = jsonata("$sum(example.value)"); 20 | var result = expression.evaluate(data); // returns 24 21 | System.out.println(result); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/main/src/main/java/MainJson.java: -------------------------------------------------------------------------------- 1 | import java.util.Map; import java.util.List; 2 | import static com.dashjoin.jsonata.Jsonata.jsonata; 3 | import com.dashjoin.jsonata.Functions; 4 | import com.dashjoin.jsonata.json.Json; 5 | 6 | public class MainJson { 7 | 8 | /** 9 | * Built-in JSON parser usage. 10 | */ 11 | public static void main(String[] args) { 12 | 13 | String json = "{\n" + // 14 | " \"example\": [\n" + // 15 | " {\n" + // 16 | " \"value\": 4\n" + // 17 | " },\n" + // 18 | " {\n" + // 19 | " \"value\": 7\n" + // 20 | " },\n" + // 21 | " {\n" + // 22 | " \"value\": 13\n" + // 23 | " }\n" + // 24 | " ]\n" + // 25 | "}"; 26 | 27 | var data = Json.parseJson(json); 28 | 29 | System.out.println(Functions.string(data,true)); 30 | 31 | var expression = jsonata("$sum(example.value)"); 32 | var result = expression.evaluate(data); // returns 24 33 | System.out.println(result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/JException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.dashjoin.jsonata; 19 | 20 | import java.util.IllegalFormatException; 21 | import java.util.List; 22 | import java.util.regex.Matcher; 23 | 24 | public class JException extends RuntimeException { 25 | 26 | private static final long serialVersionUID = -3354943281127831704L; 27 | 28 | String error; 29 | int location; 30 | Object current; 31 | Object expected; 32 | 33 | public JException(String error) { 34 | this(error, -1, null, null); 35 | } 36 | public JException(String error, int location) { 37 | this(error, location, null, null); 38 | } 39 | public JException(String error, int location, Object currentToken) { 40 | this(error, location, currentToken, null); 41 | } 42 | public JException(String error, int location, Object currentToken, Object expected) { 43 | this(null, error, location, currentToken, expected); 44 | } 45 | public JException(Throwable cause, String error, int location, Object currentToken, Object expected) { 46 | super(msg(error, location, currentToken, expected), cause); 47 | this.error = error; this.location = location; 48 | this.current = currentToken; 49 | this.expected = expected; 50 | } 51 | 52 | /** 53 | * Returns the error code, i.e. S0201 54 | * @return 55 | */ 56 | public String getError() { 57 | return error; 58 | } 59 | 60 | /** 61 | * Returns the error location (in characters) 62 | * @return 63 | */ 64 | public int getLocation() { 65 | return location; 66 | } 67 | 68 | /** 69 | * Returns the current token 70 | * @return 71 | */ 72 | public Object getCurrent() { 73 | return current; 74 | } 75 | 76 | /** 77 | * Returns the expected token 78 | * @return 79 | */ 80 | public Object getExpected() { 81 | return expected; 82 | } 83 | 84 | /** 85 | * Returns the error message with error details in the text. 86 | * Example: Syntax error: ")" {code=S0201 position=3} 87 | * @return 88 | */ 89 | public String getDetailedErrorMessage() { 90 | return msg(error, location, current, expected, true); 91 | } 92 | 93 | /** 94 | * Generate error message from given error code 95 | * Codes are defined in Jsonata.errorCodes 96 | * 97 | * Fallback: if error code does not exist, return a generic message 98 | * 99 | * @param error 100 | * @param location 101 | * @param arg1 102 | * @param arg2 103 | * @return 104 | */ 105 | public static String msg(String error, int location, Object arg1, Object arg2) { 106 | return msg(error, location, arg1, arg2, false); 107 | } 108 | 109 | /** 110 | * Generate error message from given error code 111 | * Codes are defined in Jsonata.errorCodes 112 | * 113 | * Fallback: if error code does not exist, return a generic message 114 | * 115 | * @param error 116 | * @param location 117 | * @param arg1 118 | * @param arg2 119 | * @param details True = add error details as text, false = don't add details (use getters to retrieve details) 120 | * @return 121 | */ 122 | public static String msg(String error, int location, Object arg1, Object arg2, boolean details) { 123 | String message = Jsonata.errorCodes.get(error); 124 | 125 | if (message==null) { 126 | // unknown error code 127 | return "JSonataException "+error+ 128 | (details ? " {code=unknown position="+location+" arg1="+arg1+" arg2="+arg2+"}" : ""); 129 | } 130 | 131 | String formatted = message; 132 | try { 133 | // Replace any {{var}} with Java format "%1$s" 134 | formatted = formatted.replaceFirst("\\{\\{\\w+\\}\\}", Matcher.quoteReplacement("\"%1$s\"")); 135 | formatted = formatted.replaceFirst("\\{\\{\\w+\\}\\}", Matcher.quoteReplacement("\"%2$s\"")); 136 | 137 | formatted = String.format(formatted, arg1, arg2); 138 | } catch (IllegalFormatException ex) { 139 | ex.printStackTrace(); 140 | // ignore 141 | } 142 | if (details) { 143 | formatted = formatted + " {code="+error; 144 | if (location>=0) 145 | formatted += " position="+location; 146 | formatted += "}"; 147 | } 148 | return formatted; 149 | } 150 | 151 | // Recover 152 | String type; 153 | List remaining; 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/Timebox.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.dashjoin.jsonata; 19 | 20 | import com.dashjoin.jsonata.Jsonata.Frame; 21 | 22 | /** 23 | * Configure max runtime / max recursion depth. 24 | * See Frame.setRuntimeBounds - usually not used directly 25 | */ 26 | public class Timebox { 27 | 28 | long timeout = 5000L; 29 | int maxDepth = 100; 30 | 31 | long time = System.currentTimeMillis(); 32 | int depth = 0; 33 | 34 | /** 35 | * Protect the process/browser from a runnaway expression 36 | * i.e. Infinite loop (tail recursion), or excessive stack growth 37 | * 38 | * @param {Object} expr - expression to protect 39 | * @param {Number} timeout - max time in ms 40 | * @param {Number} maxDepth - max stack depth 41 | */ 42 | public Timebox(Frame expr) { 43 | this(expr, 5000L, 100); 44 | } 45 | 46 | public Timebox(Frame expr, long timeout, int maxDepth) { 47 | this.timeout = timeout; 48 | this.maxDepth = maxDepth; 49 | 50 | // register callbacks 51 | expr.setEvaluateEntryCallback( (_exp, _input, _env)-> { 52 | if (_env.isParallelCall) return; 53 | depth++; 54 | checkRunnaway(); 55 | }); 56 | expr.setEvaluateExitCallback( (_exp, _input, _env, _res)-> { 57 | if (_env.isParallelCall) return; 58 | depth--; 59 | checkRunnaway(); 60 | }); 61 | } 62 | 63 | void checkRunnaway() { 64 | if (depth > maxDepth) { 65 | // stack too deep 66 | throw new JException("Stack overflow error: Check for non-terminating recursive function. Consider rewriting as tail-recursive. Depth="+depth+" max="+maxDepth,-1); 67 | //stack: new Error().stack, 68 | //code: "U1001" 69 | //}; 70 | } 71 | if (System.currentTimeMillis() - time > timeout) { 72 | // expression has run for too long 73 | throw new JException("Expression evaluation timeout: Check for infinite loop",-1); 74 | //stack: new Error().stack, 75 | //code: "U1001" 76 | //}; 77 | } 78 | }; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/Tokenizer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Derived from Javascript code under this license: 20 | /** 21 | * © Copyright IBM Corp. 2016, 2018 All Rights Reserved 22 | * Project name: JSONata 23 | * This project is licensed under the MIT License, see LICENSE 24 | */ 25 | package com.dashjoin.jsonata; 26 | 27 | import java.util.HashMap; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | public class Tokenizer { // = function (path) { 32 | 33 | static HashMap operators = new HashMap() {{ 34 | put(".", 75); 35 | put("[", 80); 36 | put("]", 0); 37 | put("{", 70); 38 | put("}", 0); 39 | put("(", 80); 40 | put(")", 0); 41 | put(",", 0); 42 | put("@", 80); 43 | put("#", 80); 44 | put(";", 80); 45 | put(":", 80); 46 | put("?", 20); 47 | put("+", 50); 48 | put("-", 50); 49 | put("*", 60); 50 | put("/", 60); 51 | put("%", 60); 52 | put("|", 20); 53 | put("=", 40); 54 | put("<", 40); 55 | put(">", 40); 56 | put("^", 40); 57 | put("**", 60); 58 | put("..", 20); 59 | put(":=", 10); 60 | put("!=", 40); 61 | put("<=", 40); 62 | put(">=", 40); 63 | put("~>", 40); 64 | put("and", 30); 65 | put("or", 25); 66 | put("in", 40); 67 | put("&", 50); 68 | put("!", 0); 69 | put("~", 0); 70 | }}; 71 | 72 | static HashMap escapes = new HashMap() {{ 73 | // JSON string escape sequences - see json.org 74 | put("\"", "\""); 75 | put("\\", "\\"); 76 | put("/", "/"); 77 | put("b", "\b"); 78 | put("f", "\f"); 79 | put("n", "\n"); 80 | put("r", "\r"); 81 | put("t", "\t"); 82 | }}; 83 | 84 | // Tokenizer (lexer) - invoked by the parser to return one token at a time 85 | String path; 86 | int position = 0; 87 | int length; // = path.length; 88 | 89 | Tokenizer(String path) { 90 | this.path = path; 91 | length = path.length(); 92 | } 93 | 94 | public static class Token { 95 | String type; 96 | Object value; 97 | int position; 98 | // 99 | String id; 100 | } 101 | 102 | Token create(String type, Object value) { 103 | Token t = new Token(); 104 | t.type = type; t.value = value; t.position = position; 105 | return t; 106 | } 107 | 108 | boolean isClosingSlash(int position) { 109 | if (path.charAt(position) == '/' && depth == 0) { 110 | int backslashCount = 0; 111 | while (path.charAt(position - (backslashCount + 1)) == '\\') { 112 | backslashCount++; 113 | } 114 | if (backslashCount % 2 == 0) { 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | int depth; 122 | 123 | Pattern scanRegex() { 124 | // the prefix '/' will have been previously scanned. Find the end of the regex. 125 | // search for closing '/' ignoring any that are escaped, or within brackets 126 | int start = position; 127 | //int depth = 0; 128 | String pattern; 129 | String flags; 130 | 131 | while (position < length) { 132 | char currentChar = path.charAt(position); 133 | if (isClosingSlash(position)) { 134 | // end of regex found 135 | pattern = path.substring(start, position); 136 | if (pattern.equals("")) { 137 | throw new JException("S0301", position); 138 | } 139 | position++; 140 | currentChar = path.charAt(position); 141 | // flags 142 | start = position; 143 | while (currentChar == 'i' || currentChar == 'm') { 144 | position++; 145 | currentChar = path.charAt(position); 146 | } 147 | flags = path.substring(start, position) + 'g'; 148 | 149 | // Convert flags to Java Pattern flags 150 | int _flags = 0; 151 | if (flags.contains("i")) 152 | _flags |= Pattern.CASE_INSENSITIVE; 153 | if (flags.contains("m")) 154 | _flags |= Pattern.MULTILINE; 155 | return Pattern.compile(pattern, _flags); // Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); 156 | } 157 | if ((currentChar == '(' || currentChar == '[' || currentChar == '{') && path.charAt(position - 1) != '\\') { 158 | depth++; 159 | } 160 | if ((currentChar == ')' || currentChar == ']' || currentChar == '}') && path.charAt(position - 1) != '\\') { 161 | depth--; 162 | } 163 | position++; 164 | } 165 | throw new JException("S0302", position); 166 | }; 167 | 168 | Token next(boolean prefix) { 169 | if (position >= length) return null; 170 | char currentChar = path.charAt(position); 171 | // skip whitespace 172 | while (position < length && " \t\n\r".indexOf(currentChar) > -1) { // Uli: removed \v as Java doesn't support it 173 | position++; 174 | if (position >= length) return null; // Uli: JS relies on charAt returns null 175 | currentChar = path.charAt(position); 176 | } 177 | // skip comments 178 | if (currentChar == '/' && path.charAt(position + 1) == '*') { 179 | var commentStart = position; 180 | position += 2; 181 | currentChar = path.charAt(position); 182 | while (!(currentChar == '*' && path.charAt(position + 1) == '/')) { 183 | currentChar = path.charAt(++position); 184 | if (position >= length) { 185 | // no closing tag 186 | throw new JException("S0106", commentStart); 187 | } 188 | } 189 | position += 2; 190 | currentChar = path.charAt(position); 191 | return next(prefix); // need this to swallow any following whitespace 192 | } 193 | // test for regex 194 | if (prefix != true && currentChar == '/') { 195 | position++; 196 | return create("regex", scanRegex()); 197 | } 198 | // handle double-char operators 199 | boolean haveMore = position < path.length()-1; // Java: position+1 is valid 200 | if (currentChar == '.' && haveMore && path.charAt(position + 1) == '.') { 201 | // double-dot .. range operator 202 | position += 2; 203 | return create("operator", ".."); 204 | } 205 | if (currentChar == ':' && haveMore && path.charAt(position + 1) == '=') { 206 | // := assignment 207 | position += 2; 208 | return create("operator", ":="); 209 | } 210 | if (currentChar == '!' && haveMore && path.charAt(position + 1) == '=') { 211 | // != 212 | position += 2; 213 | return create("operator", "!="); 214 | } 215 | if (currentChar == '>' && haveMore && path.charAt(position + 1) == '=') { 216 | // >= 217 | position += 2; 218 | return create("operator", ">="); 219 | } 220 | if (currentChar == '<' && haveMore && path.charAt(position + 1) == '=') { 221 | // <= 222 | position += 2; 223 | return create("operator", "<="); 224 | } 225 | if (currentChar == '*' && haveMore && path.charAt(position + 1) == '*') { 226 | // ** descendant wildcard 227 | position += 2; 228 | return create("operator", "**"); 229 | } 230 | if (currentChar == '~' && haveMore && path.charAt(position + 1) == '>') { 231 | // ~> chain function 232 | position += 2; 233 | return create("operator", "~>"); 234 | } 235 | // test for single char operators 236 | if (operators.get(""+currentChar)!=null) { 237 | position++; 238 | return create("operator", currentChar); 239 | } 240 | // test for string literals 241 | if (currentChar == '"' || currentChar == '\'') { 242 | char quoteType = currentChar; 243 | // double quoted string literal - find end of string 244 | position++; 245 | var qstr = ""; 246 | while (position < length) { 247 | currentChar = path.charAt(position); 248 | if (currentChar == '\\') { // escape sequence 249 | position++; 250 | currentChar = path.charAt(position); 251 | if (escapes.get(""+currentChar)!=null) { 252 | qstr += escapes.get(""+currentChar); 253 | } else if (currentChar == 'u') { 254 | // u should be followed by 4 hex digits 255 | String octets = path.substring(position + 1, (position + 1) + 4); 256 | if (octets.matches("^[0-9a-fA-F]+$")) { // /^[0-9a-fA-F]+$/.test(octets)) { 257 | int codepoint = Integer.parseInt(octets, 16); 258 | qstr += Character.toString((char) codepoint); 259 | position += 4; 260 | } else { 261 | throw new JException("S0104", position); 262 | } 263 | } else { 264 | // illegal escape sequence 265 | throw new JException("S0301", position, currentChar); 266 | 267 | } 268 | } else if (currentChar == quoteType) { 269 | position++; 270 | return create("string", qstr); 271 | } else { 272 | qstr += currentChar; 273 | } 274 | position++; 275 | } 276 | throw new JException("S0101", position); 277 | } 278 | // test for numbers 279 | Pattern numregex = Pattern.compile("^-?(0|([1-9][0-9]*))(\\.[0-9]+)?([Ee][-+]?[0-9]+)?"); 280 | Matcher match = numregex.matcher(path.substring(position)); 281 | if (match.find()) { 282 | double num = Double.parseDouble(match.group(0)); 283 | if (!Double.isNaN(num) && Double.isFinite(num)) { 284 | position += match.group(0).length(); 285 | // If the number is integral, use long as type 286 | return create("number", Utils.convertNumber(num)); 287 | } else { 288 | throw new JException("S0102", position); //, match.group[0]); 289 | } 290 | } 291 | 292 | // test for quoted names (backticks) 293 | String name; 294 | if (currentChar == '`') { 295 | // scan for closing quote 296 | position++; 297 | int end = path.indexOf('`', position); 298 | if (end != -1) { 299 | name = path.substring(position, end); 300 | position = end + 1; 301 | return create("name", name); 302 | } 303 | position = length; 304 | throw new JException("S0105", position); 305 | } 306 | // test for names 307 | int i = position; 308 | char ch; 309 | while (true) { 310 | //if (i>=length) return null; // Uli: JS relies on charAt returns null 311 | 312 | ch = i -1 || operators.containsKey(""+ch)) { // Uli: removed \v 314 | if (path.charAt(position) == '$') { 315 | // variable reference 316 | String _name = path.substring(position + 1, i); 317 | position = i; 318 | return create("variable", _name); 319 | } else { 320 | String _name = path.substring(position, i); 321 | position = i; 322 | switch (_name) { 323 | case "or": 324 | case "in": 325 | case "and": 326 | return create("operator", _name); 327 | case "true": 328 | return create("value", true); 329 | case "false": 330 | return create("value", false); 331 | case "null": 332 | return create("value", null); 333 | default: 334 | if (position == length && _name.equals("")) { 335 | // whitespace at end of input 336 | return null; 337 | } 338 | return create("name", _name); 339 | } 340 | } 341 | } else { 342 | i++; 343 | } 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/Utils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.dashjoin.jsonata; 19 | 20 | import java.util.AbstractList; 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Map.Entry; 26 | 27 | import com.dashjoin.jsonata.Jsonata.JFunction; 28 | import com.dashjoin.jsonata.Jsonata.JFunctionCallable; 29 | 30 | @SuppressWarnings({"rawtypes"}) 31 | public class Utils { 32 | public static boolean isNumeric(Object v) { 33 | if (v instanceof Long) return true; 34 | boolean isNum = false; 35 | if (v instanceof Number) { 36 | double d = ((Number)v).doubleValue(); 37 | isNum = !Double.isNaN(d); 38 | if (isNum && !Double.isFinite(d)) { 39 | throw new JException("D1001", 0, v); 40 | } 41 | } 42 | return isNum; 43 | } 44 | 45 | public static boolean isArrayOfStrings(Object v) { 46 | boolean result = false; 47 | if (v instanceof Collection) { 48 | for (Object o : ((Collection)v)) 49 | if (!(o instanceof String)) 50 | return false; 51 | return true; 52 | } 53 | return false; 54 | } 55 | public static boolean isArrayOfNumbers(Object v) { 56 | boolean result = false; 57 | if (v instanceof Collection) { 58 | for (Object o : ((Collection)v)) 59 | if (!isNumeric(o)) 60 | return false; 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | public static boolean isFunction(Object o) { 67 | return o instanceof JFunction || o instanceof JFunctionCallable; 68 | } 69 | 70 | static Object NONE = new Object(); 71 | 72 | /** 73 | * Create an empty sequence to contain query results 74 | * @returns {Array} - empty sequence 75 | */ 76 | public static List createSequence() { return createSequence(NONE); } 77 | 78 | public static List createSequence(Object el) { 79 | JList sequence = new JList<>(); 80 | sequence.sequence = true; 81 | if (el!=NONE) { 82 | if (el instanceof List && ((List)el).size()==1) 83 | sequence.add(((List)el).get(0)); 84 | else 85 | // This case does NOT exist in Javascript! Why? 86 | sequence.add(el); 87 | } 88 | return sequence; 89 | } 90 | 91 | public static class JList extends ArrayList { 92 | public JList() { super(); } 93 | public JList(int capacity) { super(capacity); } 94 | public JList(Collection c) { 95 | super(c); 96 | } 97 | 98 | // Jsonata specific flags 99 | public boolean sequence; 100 | 101 | public boolean outerWrapper; 102 | 103 | public boolean tupleStream; 104 | 105 | public boolean keepSingleton; 106 | 107 | public boolean cons; 108 | } 109 | 110 | public static boolean isSequence(Object result) { 111 | return result instanceof JList && ((JList)result).sequence; 112 | } 113 | 114 | /** 115 | * List representing an int range [a,b] 116 | * Both sides are included. Read-only + immutable. 117 | * 118 | * Used for late materialization of ranges. 119 | */ 120 | public static class RangeList extends AbstractList { 121 | 122 | final long a, b; 123 | final int size; 124 | 125 | public RangeList(long left, long right) { 126 | assert(left<=right); 127 | assert(right-left < Integer.MAX_VALUE); 128 | a = left; b = right; 129 | size = (int) (b-a+1); 130 | } 131 | 132 | @Override 133 | public int size() { 134 | return size; 135 | } 136 | 137 | @Override 138 | public boolean addAll(Collection c) { 139 | throw new UnsupportedOperationException("RangeList does not support 'addAll'"); 140 | } 141 | 142 | @Override 143 | public Number get(int index) { 144 | if (index < size) { 145 | try { 146 | return Utils.convertNumber( a + index ); 147 | } catch (JException e) { 148 | // TODO Auto-generated catch block 149 | e.printStackTrace(); 150 | } 151 | } 152 | throw new IndexOutOfBoundsException(index); 153 | } 154 | } 155 | 156 | public static Number convertNumber(Number n) { 157 | // Use long if the number is not fractional 158 | if (!isNumeric(n)) return null; 159 | if (n.longValue()==n.doubleValue()) { 160 | long l = n.longValue(); 161 | if (((int)l)==l) 162 | return (int)l; 163 | else 164 | return l; 165 | } 166 | return n.doubleValue(); 167 | } 168 | 169 | public static void checkUrl(String str) { 170 | boolean isHigh = false; 171 | for ( int i=0; i res) { 186 | for (Entry e : res.entrySet()) { 187 | Object val = e.getValue(); 188 | Object l = convertValue(val); 189 | if (l!=val) 190 | e.setValue(l); 191 | recurse(val); 192 | } 193 | } 194 | 195 | static void convertNulls(List res) { 196 | for (int i=0; i= '\u0080' && c < '\u00a0') 261 | || (c >= '\u2000' && c < '\u2100')) { 262 | w.append("\\u"); 263 | hhhh = Integer.toHexString(c); 264 | w.append("0000", 0, 4 - hhhh.length()); 265 | w.append(hhhh); 266 | } else { 267 | w.append(c); 268 | } 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/json/Json.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.dashjoin.jsonata.json; 19 | 20 | import java.io.IOException; 21 | import java.io.Reader; 22 | import java.util.ArrayList; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import com.dashjoin.jsonata.JException; 28 | import com.dashjoin.jsonata.Utils; 29 | 30 | /** 31 | * Vanilla JSON parser 32 | * 33 | * Uses classes JsonParser + JsonHandler from: 34 | * https://github.com/ralfstx/minimal-json 35 | */ 36 | public class Json { 37 | 38 | public static class _JsonHandler extends JsonHandler, Map> { 39 | protected Object value; 40 | 41 | @Override 42 | public List startArray() { 43 | return new ArrayList<>(); 44 | } 45 | 46 | @Override 47 | public Map startObject() { 48 | return new LinkedHashMap<>(); 49 | } 50 | 51 | @Override 52 | public void endNull() { 53 | value = null; 54 | } 55 | 56 | @Override 57 | public void endBoolean(boolean bool) { 58 | value = bool; 59 | } 60 | 61 | @Override 62 | public void endString(String string) { 63 | value = string; 64 | } 65 | 66 | @Override 67 | public void endNumber(String string) { 68 | double d = Double.valueOf(string); 69 | try { 70 | value = Utils.convertNumber(d); 71 | } catch (JException e) { 72 | // TODO Auto-generated catch block 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | @Override 78 | public void endArray(List array) { 79 | value = array; 80 | } 81 | 82 | @Override 83 | public void endObject(Map object) { 84 | value = object; 85 | } 86 | 87 | @Override 88 | public void endArrayValue(List array) { 89 | array.add(value); 90 | } 91 | 92 | @Override 93 | public void endObjectValue(Map object, String name) { 94 | object.put(name, value); 95 | } 96 | 97 | public Object getValue() { 98 | return value; 99 | } 100 | 101 | } 102 | 103 | /** 104 | * Parses the given JSON string 105 | * 106 | * @param json 107 | * @return Parsed object 108 | */ 109 | public static Object parseJson(String json) { 110 | _JsonHandler handler = new _JsonHandler(); 111 | JsonParser jp = new JsonParser(handler); 112 | jp.parse(json); 113 | return handler.getValue(); 114 | } 115 | 116 | /** 117 | * Parses the given JSON 118 | * 119 | * @param json 120 | * @return Parsed object 121 | * @throws IOException 122 | */ 123 | public static Object parseJson(Reader json) throws IOException { 124 | _JsonHandler handler = new _JsonHandler(); 125 | JsonParser jp = new JsonParser(handler); 126 | jp.parse(json, 65536); 127 | return handler.getValue(); 128 | } 129 | 130 | public static void main(String[] args) throws Throwable { 131 | 132 | _JsonHandler handler = new _JsonHandler(); 133 | 134 | JsonParser jp = new JsonParser(handler); 135 | 136 | jp.parse("{\"a\":false}"); 137 | 138 | System.out.println(handler.getValue()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/json/JsonHandler.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 EclipseSource. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | ******************************************************************************/ 22 | package com.dashjoin.jsonata.json; 23 | //package com.eclipsesource.json; 24 | 25 | 26 | /** 27 | * A handler for parser events. Instances of this class can be given to a {@link JsonParser}. The 28 | * parser will then call the methods of the given handler while reading the input. 29 | *

30 | * The default implementations of these methods do nothing. Subclasses may override only those 31 | * methods they are interested in. They can use getLocation() to access the current 32 | * character position of the parser at any point. The start* methods will be called 33 | * while the location points to the first character of the parsed element. The end* 34 | * methods will be called while the location points to the character position that directly follows 35 | * the last character of the parsed element. Example: 36 | *

37 | * 38 | *
 39 |  * ["lorem ipsum"]
 40 |  *  ^            ^
 41 |  *  startString  endString
 42 |  * 
43 | *

44 | * Subclasses that build an object representation of the parsed JSON can return arbitrary handler 45 | * objects for JSON arrays and JSON objects in {@link #startArray()} and {@link #startObject()}. 46 | * These handler objects will then be provided in all subsequent parser events for this particular 47 | * array or object. They can be used to keep track the elements of a JSON array or object. 48 | *

49 | * 50 | * @param 51 | * The type of handlers used for JSON arrays 52 | * @param 53 | * The type of handlers used for JSON objects 54 | * @see JsonParser 55 | */ 56 | public abstract class JsonHandler { 57 | 58 | JsonParser parser; 59 | 60 | /** 61 | * Returns the current parser location. 62 | * 63 | * @return the current parser location 64 | */ 65 | protected Location getLocation() { 66 | return parser.getLocation(); 67 | } 68 | 69 | /** 70 | * Indicates the beginning of a null literal in the JSON input. This method will be 71 | * called when reading the first character of the literal. 72 | */ 73 | public void startNull() { 74 | } 75 | 76 | /** 77 | * Indicates the end of a null literal in the JSON input. This method will be called 78 | * after reading the last character of the literal. 79 | */ 80 | public void endNull() { 81 | } 82 | 83 | /** 84 | * Indicates the beginning of a boolean literal (true or false) in the 85 | * JSON input. This method will be called when reading the first character of the literal. 86 | */ 87 | public void startBoolean() { 88 | } 89 | 90 | /** 91 | * Indicates the end of a boolean literal (true or false) in the JSON 92 | * input. This method will be called after reading the last character of the literal. 93 | * 94 | * @param value 95 | * the parsed boolean value 96 | */ 97 | public void endBoolean(boolean value) { 98 | } 99 | 100 | /** 101 | * Indicates the beginning of a string in the JSON input. This method will be called when reading 102 | * the opening double quote character ('"'). 103 | */ 104 | public void startString() { 105 | } 106 | 107 | /** 108 | * Indicates the end of a string in the JSON input. This method will be called after reading the 109 | * closing double quote character ('"'). 110 | * 111 | * @param string 112 | * the parsed string 113 | */ 114 | public void endString(String string) { 115 | } 116 | 117 | /** 118 | * Indicates the beginning of a number in the JSON input. This method will be called when reading 119 | * the first character of the number. 120 | */ 121 | public void startNumber() { 122 | } 123 | 124 | /** 125 | * Indicates the end of a number in the JSON input. This method will be called after reading the 126 | * last character of the number. 127 | * 128 | * @param string 129 | * the parsed number string 130 | */ 131 | public void endNumber(String string) { 132 | } 133 | 134 | /** 135 | * Indicates the beginning of an array in the JSON input. This method will be called when reading 136 | * the opening square bracket character ('['). 137 | *

138 | * This method may return an object to handle subsequent parser events for this array. This array 139 | * handler will then be provided in all calls to {@link #startArrayValue(Object) 140 | * startArrayValue()}, {@link #endArrayValue(Object) endArrayValue()}, and 141 | * {@link #endArray(Object) endArray()} for this array. 142 | *

143 | * 144 | * @return a handler for this array, or null if not needed 145 | */ 146 | public A startArray() { 147 | return null; 148 | } 149 | 150 | /** 151 | * Indicates the end of an array in the JSON input. This method will be called after reading the 152 | * closing square bracket character (']'). 153 | * 154 | * @param array 155 | * the array handler returned from {@link #startArray()}, or null if not 156 | * provided 157 | */ 158 | public void endArray(A array) { 159 | } 160 | 161 | /** 162 | * Indicates the beginning of an array element in the JSON input. This method will be called when 163 | * reading the first character of the element, just before the call to the start 164 | * method for the specific element type ({@link #startString()}, {@link #startNumber()}, etc.). 165 | * 166 | * @param array 167 | * the array handler returned from {@link #startArray()}, or null if not 168 | * provided 169 | */ 170 | public void startArrayValue(A array) { 171 | } 172 | 173 | /** 174 | * Indicates the end of an array element in the JSON input. This method will be called after 175 | * reading the last character of the element value, just after the end method for the 176 | * specific element type (like {@link #endString(String) endString()}, {@link #endNumber(String) 177 | * endNumber()}, etc.). 178 | * 179 | * @param array 180 | * the array handler returned from {@link #startArray()}, or null if not 181 | * provided 182 | */ 183 | public void endArrayValue(A array) { 184 | } 185 | 186 | /** 187 | * Indicates the beginning of an object in the JSON input. This method will be called when reading 188 | * the opening curly bracket character ('{'). 189 | *

190 | * This method may return an object to handle subsequent parser events for this object. This 191 | * object handler will be provided in all calls to {@link #startObjectName(Object) 192 | * startObjectName()}, {@link #endObjectName(Object, String) endObjectName()}, 193 | * {@link #startObjectValue(Object, String) startObjectValue()}, 194 | * {@link #endObjectValue(Object, String) endObjectValue()}, and {@link #endObject(Object) 195 | * endObject()} for this object. 196 | *

197 | * 198 | * @return a handler for this object, or null if not needed 199 | */ 200 | public O startObject() { 201 | return null; 202 | } 203 | 204 | /** 205 | * Indicates the end of an object in the JSON input. This method will be called after reading the 206 | * closing curly bracket character ('}'). 207 | * 208 | * @param object 209 | * the object handler returned from {@link #startObject()}, or null if not provided 210 | */ 211 | public void endObject(O object) { 212 | } 213 | 214 | /** 215 | * Indicates the beginning of the name of an object member in the JSON input. This method will be 216 | * called when reading the opening quote character ('"') of the member name. 217 | * 218 | * @param object 219 | * the object handler returned from {@link #startObject()}, or null if not 220 | * provided 221 | */ 222 | public void startObjectName(O object) { 223 | } 224 | 225 | /** 226 | * Indicates the end of an object member name in the JSON input. This method will be called after 227 | * reading the closing quote character ('"') of the member name. 228 | * 229 | * @param object 230 | * the object handler returned from {@link #startObject()}, or null if not provided 231 | * @param name 232 | * the parsed member name 233 | */ 234 | public void endObjectName(O object, String name) { 235 | } 236 | 237 | /** 238 | * Indicates the beginning of the name of an object member in the JSON input. This method will be 239 | * called when reading the opening quote character ('"') of the member name. 240 | * 241 | * @param object 242 | * the object handler returned from {@link #startObject()}, or null if not 243 | * provided 244 | * @param name 245 | * the member name 246 | */ 247 | public void startObjectValue(O object, String name) { 248 | } 249 | 250 | /** 251 | * Indicates the end of an object member value in the JSON input. This method will be called after 252 | * reading the last character of the member value, just after the end method for the 253 | * specific member type (like {@link #endString(String) endString()}, {@link #endNumber(String) 254 | * endNumber()}, etc.). 255 | * 256 | * @param object 257 | * the object handler returned from {@link #startObject()}, or null if not provided 258 | * @param name 259 | * the parsed member name 260 | */ 261 | public void endObjectValue(O object, String name) { 262 | } 263 | 264 | } -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/json/JsonParser.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2013, 2016 EclipseSource. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | ******************************************************************************/ 22 | package com.dashjoin.jsonata.json; 23 | //package com.eclipsesource.json; 24 | 25 | import java.io.IOException; 26 | import java.io.Reader; 27 | import java.io.StringReader; 28 | 29 | 30 | /** 31 | * A streaming parser for JSON text. The parser reports all events to a given handler. 32 | */ 33 | public class JsonParser { 34 | 35 | private static final int MAX_NESTING_LEVEL = 1000; 36 | private static final int MIN_BUFFER_SIZE = 10; 37 | private static final int DEFAULT_BUFFER_SIZE = 1024; 38 | 39 | private final JsonHandler handler; 40 | private Reader reader; 41 | private char[] buffer; 42 | private int bufferOffset; 43 | private int index; 44 | private int fill; 45 | private int line; 46 | private int lineOffset; 47 | private int current; 48 | private StringBuilder captureBuffer; 49 | private int captureStart; 50 | private int nestingLevel; 51 | 52 | /* 53 | * | bufferOffset 54 | * v 55 | * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t] < input 56 | * [l|m|n|o|p|q|r|s|t|?|?] < buffer 57 | * ^ ^ 58 | * | index fill 59 | */ 60 | 61 | /** 62 | * Creates a new JsonParser with the given handler. The parser will report all parser events to 63 | * this handler. 64 | * 65 | * @param handler 66 | * the handler to process parser events 67 | */ 68 | @SuppressWarnings("unchecked") 69 | public JsonParser(JsonHandler handler) { 70 | if (handler == null) { 71 | throw new NullPointerException("handler is null"); 72 | } 73 | this.handler = (JsonHandler)handler; 74 | handler.parser = this; 75 | } 76 | 77 | /** 78 | * Parses the given input string. The input must contain a valid JSON value, optionally padded 79 | * with whitespace. 80 | * 81 | * @param string 82 | * the input string, must be valid JSON 83 | * @throws ParseException 84 | * if the input is not valid JSON 85 | */ 86 | public void parse(String string) { 87 | if (string == null) { 88 | throw new NullPointerException("string is null"); 89 | } 90 | int bufferSize = Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length())); 91 | try { 92 | parse(new StringReader(string), bufferSize); 93 | } catch (IOException exception) { 94 | // StringReader does not throw IOException 95 | throw new RuntimeException(exception); 96 | } 97 | } 98 | 99 | /** 100 | * Reads the entire input from the given reader and parses it as JSON. The input must contain a 101 | * valid JSON value, optionally padded with whitespace. 102 | *

103 | * Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an 104 | * additional BufferedReader likely won't improve reading performance. 105 | *

106 | * 107 | * @param reader 108 | * the reader to read the input from 109 | * @throws IOException 110 | * if an I/O error occurs in the reader 111 | * @throws ParseException 112 | * if the input is not valid JSON 113 | */ 114 | public void parse(Reader reader) throws IOException { 115 | parse(reader, DEFAULT_BUFFER_SIZE); 116 | } 117 | 118 | /** 119 | * Reads the entire input from the given reader and parses it as JSON. The input must contain a 120 | * valid JSON value, optionally padded with whitespace. 121 | *

122 | * Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader 123 | * in an additional BufferedReader likely won't improve reading performance. 124 | *

125 | * 126 | * @param reader 127 | * the reader to read the input from 128 | * @param buffersize 129 | * the size of the input buffer in chars 130 | * @throws IOException 131 | * if an I/O error occurs in the reader 132 | * @throws ParseException 133 | * if the input is not valid JSON 134 | */ 135 | public void parse(Reader reader, int buffersize) throws IOException { 136 | if (reader == null) { 137 | throw new NullPointerException("reader is null"); 138 | } 139 | if (buffersize <= 0) { 140 | throw new IllegalArgumentException("buffersize is zero or negative"); 141 | } 142 | this.reader = reader; 143 | buffer = new char[buffersize]; 144 | bufferOffset = 0; 145 | index = 0; 146 | fill = 0; 147 | line = 1; 148 | lineOffset = 0; 149 | current = 0; 150 | captureStart = -1; 151 | read(); 152 | skipWhiteSpace(); 153 | readValue(); 154 | skipWhiteSpace(); 155 | if (!isEndOfText()) { 156 | throw error("Unexpected character"); 157 | } 158 | } 159 | 160 | private void readValue() throws IOException { 161 | switch (current) { 162 | case 'n': 163 | readNull(); 164 | break; 165 | case 't': 166 | readTrue(); 167 | break; 168 | case 'f': 169 | readFalse(); 170 | break; 171 | case '"': 172 | readString(); 173 | break; 174 | case '[': 175 | readArray(); 176 | break; 177 | case '{': 178 | readObject(); 179 | break; 180 | case '-': 181 | case '0': 182 | case '1': 183 | case '2': 184 | case '3': 185 | case '4': 186 | case '5': 187 | case '6': 188 | case '7': 189 | case '8': 190 | case '9': 191 | readNumber(); 192 | break; 193 | default: 194 | throw expected("value"); 195 | } 196 | } 197 | 198 | private void readArray() throws IOException { 199 | Object array = handler.startArray(); 200 | read(); 201 | if (++nestingLevel > MAX_NESTING_LEVEL) { 202 | throw error("Nesting too deep"); 203 | } 204 | skipWhiteSpace(); 205 | if (readChar(']')) { 206 | nestingLevel--; 207 | handler.endArray(array); 208 | return; 209 | } 210 | do { 211 | skipWhiteSpace(); 212 | handler.startArrayValue(array); 213 | readValue(); 214 | handler.endArrayValue(array); 215 | skipWhiteSpace(); 216 | } while (readChar(',')); 217 | if (!readChar(']')) { 218 | throw expected("',' or ']'"); 219 | } 220 | nestingLevel--; 221 | handler.endArray(array); 222 | } 223 | 224 | private void readObject() throws IOException { 225 | Object object = handler.startObject(); 226 | read(); 227 | if (++nestingLevel > MAX_NESTING_LEVEL) { 228 | throw error("Nesting too deep"); 229 | } 230 | skipWhiteSpace(); 231 | if (readChar('}')) { 232 | nestingLevel--; 233 | handler.endObject(object); 234 | return; 235 | } 236 | do { 237 | skipWhiteSpace(); 238 | handler.startObjectName(object); 239 | String name = readName(); 240 | handler.endObjectName(object, name); 241 | skipWhiteSpace(); 242 | if (!readChar(':')) { 243 | throw expected("':'"); 244 | } 245 | skipWhiteSpace(); 246 | handler.startObjectValue(object, name); 247 | readValue(); 248 | handler.endObjectValue(object, name); 249 | skipWhiteSpace(); 250 | } while (readChar(',')); 251 | if (!readChar('}')) { 252 | throw expected("',' or '}'"); 253 | } 254 | nestingLevel--; 255 | handler.endObject(object); 256 | } 257 | 258 | private String readName() throws IOException { 259 | if (current != '"') { 260 | throw expected("name"); 261 | } 262 | return readStringInternal(); 263 | } 264 | 265 | private void readNull() throws IOException { 266 | handler.startNull(); 267 | read(); 268 | readRequiredChar('u'); 269 | readRequiredChar('l'); 270 | readRequiredChar('l'); 271 | handler.endNull(); 272 | } 273 | 274 | private void readTrue() throws IOException { 275 | handler.startBoolean(); 276 | read(); 277 | readRequiredChar('r'); 278 | readRequiredChar('u'); 279 | readRequiredChar('e'); 280 | handler.endBoolean(true); 281 | } 282 | 283 | private void readFalse() throws IOException { 284 | handler.startBoolean(); 285 | read(); 286 | readRequiredChar('a'); 287 | readRequiredChar('l'); 288 | readRequiredChar('s'); 289 | readRequiredChar('e'); 290 | handler.endBoolean(false); 291 | } 292 | 293 | private void readRequiredChar(char ch) throws IOException { 294 | if (!readChar(ch)) { 295 | throw expected("'" + ch + "'"); 296 | } 297 | } 298 | 299 | private void readString() throws IOException { 300 | handler.startString(); 301 | handler.endString(readStringInternal()); 302 | } 303 | 304 | private String readStringInternal() throws IOException { 305 | read(); 306 | startCapture(); 307 | while (current != '"') { 308 | if (current == '\\') { 309 | pauseCapture(); 310 | readEscape(); 311 | startCapture(); 312 | } else if (current < 0x20) { 313 | throw expected("valid string character"); 314 | } else { 315 | read(); 316 | } 317 | } 318 | String string = endCapture(); 319 | read(); 320 | return string; 321 | } 322 | 323 | private void readEscape() throws IOException { 324 | read(); 325 | switch (current) { 326 | case '"': 327 | case '/': 328 | case '\\': 329 | captureBuffer.append((char)current); 330 | break; 331 | case 'b': 332 | captureBuffer.append('\b'); 333 | break; 334 | case 'f': 335 | captureBuffer.append('\f'); 336 | break; 337 | case 'n': 338 | captureBuffer.append('\n'); 339 | break; 340 | case 'r': 341 | captureBuffer.append('\r'); 342 | break; 343 | case 't': 344 | captureBuffer.append('\t'); 345 | break; 346 | case 'u': 347 | char[] hexChars = new char[4]; 348 | for (int i = 0; i < 4; i++) { 349 | read(); 350 | if (!isHexDigit()) { 351 | throw expected("hexadecimal digit"); 352 | } 353 | hexChars[i] = (char)current; 354 | } 355 | captureBuffer.append((char)Integer.parseInt(new String(hexChars), 16)); 356 | break; 357 | default: 358 | throw expected("valid escape sequence"); 359 | } 360 | read(); 361 | } 362 | 363 | private void readNumber() throws IOException { 364 | handler.startNumber(); 365 | startCapture(); 366 | readChar('-'); 367 | int firstDigit = current; 368 | if (!readDigit()) { 369 | throw expected("digit"); 370 | } 371 | if (firstDigit != '0') { 372 | while (readDigit()) { 373 | } 374 | } 375 | readFraction(); 376 | readExponent(); 377 | handler.endNumber(endCapture()); 378 | } 379 | 380 | private boolean readFraction() throws IOException { 381 | if (!readChar('.')) { 382 | return false; 383 | } 384 | if (!readDigit()) { 385 | throw expected("digit"); 386 | } 387 | while (readDigit()) { 388 | } 389 | return true; 390 | } 391 | 392 | private boolean readExponent() throws IOException { 393 | if (!readChar('e') && !readChar('E')) { 394 | return false; 395 | } 396 | if (!readChar('+')) { 397 | readChar('-'); 398 | } 399 | if (!readDigit()) { 400 | throw expected("digit"); 401 | } 402 | while (readDigit()) { 403 | } 404 | return true; 405 | } 406 | 407 | private boolean readChar(char ch) throws IOException { 408 | if (current != ch) { 409 | return false; 410 | } 411 | read(); 412 | return true; 413 | } 414 | 415 | private boolean readDigit() throws IOException { 416 | if (!isDigit()) { 417 | return false; 418 | } 419 | read(); 420 | return true; 421 | } 422 | 423 | private void skipWhiteSpace() throws IOException { 424 | while (isWhiteSpace()) { 425 | read(); 426 | } 427 | } 428 | 429 | private void read() throws IOException { 430 | if (index == fill) { 431 | if (captureStart != -1) { 432 | captureBuffer.append(buffer, captureStart, fill - captureStart); 433 | captureStart = 0; 434 | } 435 | bufferOffset += fill; 436 | fill = reader.read(buffer, 0, buffer.length); 437 | index = 0; 438 | if (fill == -1) { 439 | current = -1; 440 | index++; 441 | return; 442 | } 443 | } 444 | if (current == '\n') { 445 | line++; 446 | lineOffset = bufferOffset + index; 447 | } 448 | current = buffer[index++]; 449 | } 450 | 451 | private void startCapture() { 452 | if (captureBuffer == null) { 453 | captureBuffer = new StringBuilder(); 454 | } 455 | captureStart = index - 1; 456 | } 457 | 458 | private void pauseCapture() { 459 | int end = current == -1 ? index : index - 1; 460 | captureBuffer.append(buffer, captureStart, end - captureStart); 461 | captureStart = -1; 462 | } 463 | 464 | private String endCapture() { 465 | int start = captureStart; 466 | int end = index - 1; 467 | captureStart = -1; 468 | if (captureBuffer.length() > 0) { 469 | captureBuffer.append(buffer, start, end - start); 470 | String captured = captureBuffer.toString(); 471 | captureBuffer.setLength(0); 472 | return captured; 473 | } 474 | return new String(buffer, start, end - start); 475 | } 476 | 477 | Location getLocation() { 478 | int offset = bufferOffset + index - 1; 479 | int column = offset - lineOffset + 1; 480 | return new Location(offset, line, column); 481 | } 482 | 483 | private ParseException expected(String expected) { 484 | if (isEndOfText()) { 485 | return error("Unexpected end of input"); 486 | } 487 | return error("Expected " + expected); 488 | } 489 | 490 | private ParseException error(String message) { 491 | return new ParseException(message, getLocation()); 492 | } 493 | 494 | private boolean isWhiteSpace() { 495 | return current == ' ' || current == '\t' || current == '\n' || current == '\r'; 496 | } 497 | 498 | private boolean isDigit() { 499 | return current >= '0' && current <= '9'; 500 | } 501 | 502 | private boolean isHexDigit() { 503 | return current >= '0' && current <= '9' 504 | || current >= 'a' && current <= 'f' 505 | || current >= 'A' && current <= 'F'; 506 | } 507 | 508 | private boolean isEndOfText() { 509 | return current == -1; 510 | } 511 | 512 | } -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/json/Location.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata.json; 2 | /******************************************************************************* 3 | * Copyright (c) 2016 EclipseSource. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | ******************************************************************************/ 23 | //package com.eclipsesource.json; 24 | 25 | 26 | /** 27 | * An immutable object that represents a location in the parsed text. 28 | */ 29 | public class Location { 30 | 31 | /** 32 | * The absolute character index, starting at 0. 33 | */ 34 | public final int offset; 35 | 36 | /** 37 | * The line number, starting at 1. 38 | */ 39 | public final int line; 40 | 41 | /** 42 | * The column number, starting at 1. 43 | */ 44 | public final int column; 45 | 46 | Location(int offset, int line, int column) { 47 | this.offset = offset; 48 | this.column = column; 49 | this.line = line; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return line + ":" + column; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return offset; 60 | } 61 | 62 | @Override 63 | public boolean equals(Object obj) { 64 | if (this == obj) { 65 | return true; 66 | } 67 | if (obj == null) { 68 | return false; 69 | } 70 | if (getClass() != obj.getClass()) { 71 | return false; 72 | } 73 | Location other = (Location)obj; 74 | return offset == other.offset && column == other.column && line == other.line; 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/json/ParseException.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata.json; 2 | /******************************************************************************* 3 | * Copyright (c) 2013, 2016 EclipseSource. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | ******************************************************************************/ 23 | //package com.eclipsesource.json; 24 | 25 | /** 26 | * An unchecked exception to indicate that an input does not qualify as valid JSON. 27 | */ 28 | @SuppressWarnings("serial") // use default serial UID 29 | public class ParseException extends RuntimeException { 30 | 31 | private final Location location; 32 | 33 | ParseException(String message, Location location) { 34 | super(message + " at " + location); 35 | this.location = location; 36 | } 37 | 38 | /** 39 | * Returns the location at which the error occurred. 40 | * 41 | * @return the error location 42 | */ 43 | public Location getLocation() { 44 | return location; 45 | } 46 | 47 | /** 48 | * Returns the absolute character index at which the error occurred. The offset of the first 49 | * character of a document is 0. 50 | * 51 | * @return the character offset at which the error occurred, will be >= 0 52 | * @deprecated Use {@link #getLocation()} instead 53 | */ 54 | @Deprecated 55 | public int getOffset() { 56 | return location.offset; 57 | } 58 | 59 | /** 60 | * Returns the line number in which the error occurred. The number of the first line is 1. 61 | * 62 | * @return the line in which the error occurred, will be >= 1 63 | * @deprecated Use {@link #getLocation()} instead 64 | */ 65 | @Deprecated 66 | public int getLine() { 67 | return location.line; 68 | } 69 | 70 | /** 71 | * Returns the column number at which the error occurred, i.e. the number of the character in its 72 | * line. The number of the first character of a line is 1. 73 | * 74 | * @return the column in which the error occurred, will be >= 1 75 | * @deprecated Use {@link #getLocation()} instead 76 | */ 77 | @Deprecated 78 | public int getColumn() { 79 | return location.column; 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/utils/Constants.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.dashjoin.jsonata.utils; 19 | 20 | /** 21 | * Constants required by DateTimeUtils 22 | */ 23 | public class Constants { 24 | public static final String ERR_MSG_SEQUENCE_UNSUPPORTED = "Formatting or parsing an integer as a sequence starting with %s is not supported by this implementation"; 25 | public static final String ERR_MSG_DIFF_DECIMAL_GROUP = "In a decimal digit pattern, all digits must be from the same decimal group"; 26 | public static final String ERR_MSG_NO_CLOSING_BRACKET = "No matching closing bracket ']' in date/time picture string"; 27 | public static final String ERR_MSG_UNKNOWN_COMPONENT_SPECIFIER = "Unknown component specifier %s in date/time picture string"; 28 | public static final String ERR_MSG_INVALID_NAME_MODIFIER = "The 'name' modifier can only be applied to months and days in the date/time picture string, not %s"; 29 | public static final String ERR_MSG_TIMEZONE_FORMAT = "The timezone integer format specifier cannot have more than four digits"; 30 | public static final String ERR_MSG_MISSING_FORMAT = "The date/time picture string is missing specifiers required to parse the timestamp"; 31 | public static final String ERR_MSG_INVALID_OPTIONS_SINGLE_CHAR = "Argument 3 of function %s is invalid. The value of the %s property must be a single character"; 32 | public static final String ERR_MSG_INVALID_OPTIONS_STRING = "Argument 3 of function %s is invalid. The value of the %s property must be a string"; 33 | 34 | /** 35 | * Collection of decimal format strings that defined by xsl:decimal-format. 36 | * 37 | *
38 |     *     <!ELEMENT xsl:decimal-format EMPTY>
39 |     *     <!ATTLIST xsl:decimal-format
40 |     *       name %qname; #IMPLIED
41 |     *       decimal-separator %char; "."
42 |     *       grouping-separator %char; ","
43 |     *       infinity CDATA "Infinity"
44 |     *       minus-sign %char; "-"
45 |     *       NaN CDATA "NaN"
46 |     *       percent %char; "%"
47 |     *       per-mille %char; "‰"
48 |     *       zero-digit %char; "0"
49 |     *       digit %char; "#"
50 |     *       pattern-separator %char; ";">
51 |     * 
52 | * 53 | * http://www.w3.org/TR/xslt#format-number} to explain format-number in XSLT 54 | * Specification xsl.usage advanced 55 | */ 56 | public static final String SYMBOL_DECIMAL_SEPARATOR = "decimal-separator"; 57 | public static final String SYMBOL_GROUPING_SEPARATOR = "grouping-separator"; 58 | public static final String SYMBOL_INFINITY = "infinity"; 59 | public static final String SYMBOL_MINUS_SIGN = "minus-sign"; 60 | public static final String SYMBOL_NAN = "NaN"; 61 | public static final String SYMBOL_PERCENT = "percent"; 62 | public static final String SYMBOL_PER_MILLE = "per-mille"; 63 | public static final String SYMBOL_ZERO_DIGIT = "zero-digit"; 64 | public static final String SYMBOL_DIGIT = "digit"; 65 | public static final String SYMBOL_PATTERN_SEPARATOR = "pattern-separator"; 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/dashjoin/jsonata/utils/Signature.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jsonata-java is the JSONata Java reference port 3 | * 4 | * Copyright Dashjoin GmbH. https://dashjoin.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Derived from original JSONata4Java Signature code under this license: 20 | /* 21 | * (c) Copyright 2018, 2019 IBM Corporation 22 | * 1 New Orchard Road, 23 | * Armonk, New York, 10504-1722 24 | * United States 25 | * +1 914 499 1900 26 | * support: Nathaniel Mills wnm3@us.ibm.com 27 | * 28 | * Licensed under the Apache License, Version 2.0 (the "License"); 29 | * you may not use this file except in compliance with the License. 30 | * You may obtain a copy of the License at 31 | * 32 | * http://www.apache.org/licenses/LICENSE-2.0 33 | * 34 | * Unless required by applicable law or agreed to in writing, software 35 | * distributed under the License is distributed on an "AS IS" BASIS, 36 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | * See the License for the specific language governing permissions and 38 | * limitations under the License. 39 | * 40 | */ 41 | 42 | package com.dashjoin.jsonata.utils; 43 | 44 | import java.io.Serializable; 45 | import java.util.ArrayList; 46 | import java.util.List; 47 | import java.util.Map; 48 | import java.util.regex.Matcher; 49 | import java.util.regex.Pattern; 50 | import java.util.regex.PatternSyntaxException; 51 | 52 | import javax.lang.model.type.NullType; 53 | 54 | import com.dashjoin.jsonata.Functions; 55 | import com.dashjoin.jsonata.JException; 56 | import com.dashjoin.jsonata.Utils; 57 | 58 | /** 59 | * Manages signature related functions 60 | */ 61 | public class Signature implements Serializable { 62 | 63 | private static final long serialVersionUID = -450755246855587271L; 64 | 65 | static class Param { 66 | String type; 67 | String regex; 68 | boolean context; 69 | boolean array; 70 | String subtype; 71 | String contextRegex; 72 | 73 | @Override 74 | public String toString() { 75 | return "Param "+type+" regex="+regex+" ctx="+context+" array="+array; 76 | } 77 | } 78 | 79 | Param _param = new Param(); 80 | 81 | List _params = new ArrayList<>(); 82 | Param _prevParam = _param; 83 | Pattern _regex = null; 84 | String _signature = ""; 85 | String functionName; 86 | 87 | public Signature(String signature, String function) { 88 | this.functionName = function; 89 | parseSignature(signature); 90 | } 91 | 92 | public void setFunctionName(String functionName) { 93 | this.functionName = functionName; 94 | } 95 | 96 | public static void main(String[] args) { 97 | Signature s = new Signature("", "test");//"); 98 | System.out.println(s._params); 99 | } 100 | 101 | int findClosingBracket(String str, int start, char openSymbol, char closeSymbol) { 102 | // returns the position of the closing symbol (e.g. bracket) in a string 103 | // that balances the opening symbol at position start 104 | int depth = 1; 105 | int position = start; 106 | while (position < str.length()) { 107 | position++; 108 | char symbol = str.charAt(position); 109 | if (symbol == closeSymbol) { 110 | depth--; 111 | if (depth == 0) { 112 | // we're done 113 | break; // out of while loop 114 | } 115 | } else if (symbol == openSymbol) { 116 | depth++; 117 | } 118 | } 119 | return position; 120 | }; 121 | 122 | String getSymbol(Object value) { 123 | String symbol; 124 | if (value == null) { 125 | symbol = "m"; 126 | } else { 127 | // first check to see if this is a function 128 | if (Utils.isFunction(value) || Functions.isLambda(value) || (value instanceof Pattern)) { //} instanceof JFunction || value instanceof Function) { 129 | symbol = "f"; 130 | } else if (value instanceof String) 131 | symbol = "s"; 132 | else if (value instanceof Number) 133 | symbol = "n"; 134 | else if (value instanceof Boolean) 135 | symbol = "b"; 136 | else if (value instanceof List) 137 | symbol = "a"; 138 | else if (value instanceof Map) 139 | symbol = "o"; 140 | else if (value instanceof NullType) // Uli: is this used??? 141 | symbol = "l"; 142 | else 143 | // any value can be undefined, but should be allowed to match 144 | symbol = "m"; // m for missing 145 | } 146 | return symbol; 147 | }; 148 | 149 | void next() { 150 | _params.add(_param); 151 | _prevParam = _param; 152 | _param = new Param(); 153 | } 154 | 155 | /** 156 | * Parses a function signature definition and returns a validation function 157 | * 158 | * @param {string} 159 | * signature - the signature between the 160 | * @returns validation pattern 161 | */ 162 | Pattern parseSignature(String signature) { 163 | // create a Regex that represents this signature and return a function that when 164 | // invoked, 165 | // returns the validated (possibly fixed-up) arguments, or throws a validation 166 | // error 167 | // step through the signature, one symbol at a time 168 | int position = 1; 169 | while (position < signature.length()) { 170 | char symbol = signature.charAt(position); 171 | if (symbol == ':') { 172 | // TODO figure out what to do with the return type 173 | // ignore it for now 174 | break; 175 | } 176 | 177 | switch (symbol) { 178 | case 's': // string 179 | case 'n': // number 180 | case 'b': // boolean 181 | case 'l': // not so sure about expecting null? 182 | case 'o': { // object 183 | _param.regex = ( "[" + symbol + "m]"); 184 | _param.type = ( ""+symbol); 185 | next(); 186 | break; 187 | } 188 | case 'a': { // array 189 | // normally treat any value as singleton array 190 | _param.regex = ( "[asnblfom]"); 191 | _param.type = ( ""+symbol); 192 | _param.array = ( true); 193 | next(); 194 | break; 195 | } 196 | case 'f': { // function 197 | _param.regex = ( "f"); 198 | _param.type = ( ""+symbol); 199 | next(); 200 | break; 201 | } 202 | case 'j': { // any JSON type 203 | _param.regex = ( "[asnblom]"); 204 | _param.type = ( ""+symbol); 205 | next(); 206 | break; 207 | } 208 | case 'x': { // any type 209 | _param.regex = ( "[asnblfom]"); 210 | _param.type = ( ""+symbol); 211 | next(); 212 | break; 213 | } 214 | case '-': { // use context if _param not supplied 215 | _prevParam.context = true; 216 | _prevParam.regex += "?"; 217 | break; 218 | } 219 | case '?': // optional _param 220 | case '+': { // one or more 221 | _prevParam.regex += symbol; 222 | break; 223 | } 224 | case '(': { // choice of types 225 | // search forward for matching ')' 226 | int endParen = findClosingBracket(signature, position, '(', ')'); 227 | String choice = signature.substring(position + 1, endParen); 228 | if (choice.indexOf("<") == -1) { 229 | // no _parameterized types, simple regex 230 | _param.regex = ( "[" + choice + "m]"); 231 | } else { 232 | // TODO harder 233 | throw new RuntimeException("Choice groups containing parameterized types are not supported"); 234 | } 235 | _param.type = ( "(" + choice + ")"); 236 | position = endParen; 237 | next(); 238 | break; 239 | } 240 | case '<': { // type _parameter - can only be applied to 'a' and 'f' 241 | String test = _prevParam.type; 242 | if (test != null) { 243 | String type = test;//.asText(); 244 | if (type.equals("a") || type.equals("f")) { 245 | // search forward for matching '>' 246 | int endPos = findClosingBracket(signature, position, '<', '>'); 247 | _prevParam.subtype = signature.substring(position + 1, endPos); 248 | position = endPos; 249 | } else { 250 | throw new RuntimeException("Type parameters can only be applied to functions and arrays"); 251 | } 252 | } else { 253 | throw new RuntimeException("Type parameters can only be applied to functions and arrays"); 254 | } 255 | break; 256 | } 257 | } 258 | position++; 259 | } // end while processing symbols in signature 260 | 261 | String regexStr = "^"; 262 | for (Param param : _params) { 263 | regexStr += "(" + param.regex + ")"; 264 | } 265 | regexStr += "$"; 266 | 267 | _regex = null; 268 | try { 269 | _regex = Pattern.compile(regexStr); 270 | _signature = regexStr; 271 | } catch (PatternSyntaxException pse) { 272 | throw new RuntimeException(pse.getLocalizedMessage()); 273 | } 274 | return _regex; 275 | } 276 | 277 | void throwValidationError(List badArgs, String badSig, String functionName) { 278 | // to figure out where this went wrong we need apply each component of the 279 | // regex to each argument until we get to the one that fails to match 280 | String partialPattern = "^"; 281 | 282 | int goodTo = 0; 283 | for (int index = 0; index < _params.size(); index++) { 284 | partialPattern += _params.get(index).regex; 285 | Pattern tester = Pattern.compile(partialPattern); 286 | Matcher match = tester.matcher(badSig); 287 | if (match.matches() == false) { 288 | // failed here 289 | throw new JException("T0410", -1, (goodTo+1), functionName); 290 | } 291 | goodTo = match.end(); 292 | } 293 | // if it got this far, it's probably because of extraneous arguments (we 294 | // haven't added the trailing '$' in the regex yet. 295 | throw new JException("T0410", -1, (goodTo+1), functionName); 296 | } 297 | 298 | @SuppressWarnings({"rawtypes", "unchecked"}) 299 | public Object validate(Object _args, Object context) { 300 | 301 | var result = new ArrayList<>(); 302 | 303 | var args = (List)_args; 304 | String suppliedSig = ""; 305 | for (Object arg : args) 306 | suppliedSig += getSymbol(arg); 307 | 308 | Matcher isValid = _regex.matcher(suppliedSig); 309 | if (isValid != null && isValid.matches()) { 310 | var validatedArgs = new ArrayList<>(); 311 | var argIndex = 0; 312 | int index = 0; 313 | for (Object _param : _params) { 314 | Param param = (Param)_param; 315 | var arg = argIndex 0) { 357 | var itemType = getSymbol(argArr.get(0)); 358 | if (!itemType.equals(""+param.subtype.charAt(0))) { // TODO recurse further 359 | arrayOK = false; 360 | } else { 361 | // make sure every item in the array is this type 362 | for (Object o : argArr) { 363 | if (!getSymbol(o).equals(itemType)) { 364 | arrayOK = false; 365 | break; 366 | } 367 | } 368 | } 369 | } 370 | } 371 | } 372 | if (!arrayOK) { 373 | throw new JException("T0412", -1, 374 | arg, 375 | //argIndex + 1, 376 | param.subtype//arraySignatureMapping[param.subtype] 377 | ); 378 | } 379 | // the function expects an array. If it's not one, make it so 380 | if (!single.equals("a")) { 381 | List _arg = new ArrayList<>(); _arg.add(arg); 382 | arg = _arg; 383 | } 384 | } 385 | validatedArgs.add(arg); 386 | argIndex++; 387 | } else { 388 | arg = argIndex compiler happy 399 | } 400 | 401 | public int getNumberOfArgs() { 402 | return _params.size(); 403 | } 404 | 405 | /** 406 | * Returns the minimum # of arguments. 407 | * I.e. the # of all non-optional arguments. 408 | */ 409 | public int getMinNumberOfArgs() { 410 | int res = 0; 411 | for (Param p : _params) 412 | if (!p.regex.contains("?")) 413 | res++; 414 | return res; 415 | } 416 | /* 417 | ArrayNode validate(String functionName, ExprListContext args, ExpressionsVisitor expressionVisitor) { 418 | ArrayNode result = JsonNodeFactory.instance.arrayNode(); 419 | String suppliedSig = ""; 420 | for (Iterator it = args.expr().iterator(); it.hasNext();) { 421 | ExprContext arg = it.next(); 422 | suppliedSig += getSymbol(arg); 423 | } 424 | Matcher isValid = _regex.matcher(suppliedSig); 425 | if (isValid != null) { 426 | ArrayNode validatedArgs = JsonNodeFactory.instance.arrayNode(); 427 | int argIndex = 0; 428 | int index = 0; 429 | for (Iterator it = _params.iterator(); it.hasNext();) { 430 | ObjectNode param = (ObjectNode) it.next(); 431 | JsonNode arg = expressionVisitor.visit(args.expr(argIndex)); 432 | String match = isValid.group(index + 1); 433 | if ("".equals(match)) { 434 | boolean useContext = (param.get("context") != null && param.get("context").asBoolean()); 435 | if (useContext) { 436 | // substitute context value for missing arg 437 | // first check that the context value is the right type 438 | JsonNode context = expressionVisitor.getVariable("$"); 439 | String contextType = getSymbol(context); 440 | // test contextType against the regex for this arg (without the trailing ?) 441 | if (Pattern.matches(param.get("regex").asText(), contextType)) { 442 | validatedArgs.add(context); 443 | } else { 444 | // context value not compatible with this argument 445 | throw new EvaluateRuntimeException("Context value is not a compatible type with argument \"" 446 | + argIndex + 1 + "\" of function \"" + functionName + "\""); 447 | } 448 | } else { 449 | validatedArgs.add(arg); 450 | argIndex++; 451 | } 452 | } else { 453 | // may have matched multiple args (if the regex ends with a '+' 454 | // split into single tokens 455 | String[] singles = match.split(""); 456 | for (String single : singles) { 457 | if ("a".equals(param.get("type").asText())) { 458 | if ("m".equals(single)) { 459 | // missing (undefined) 460 | arg = null; 461 | } else { 462 | arg = expressionVisitor.visit(args.expr(argIndex)); 463 | boolean arrayOK = true; 464 | // is there type information on the contents of the array? 465 | String subtype = "undefined"; 466 | JsonNode testSubType = param.get("subtype"); 467 | if (testSubType != null) { 468 | subtype = testSubType.asText(); 469 | if ("a".equals(single) == false && match.equals(subtype) == false) { 470 | arrayOK = false; 471 | } else if ("a".equals(single)) { 472 | ArrayNode argArray = (ArrayNode) arg; 473 | if (argArray.size() > 0) { 474 | String itemType = getSymbol(argArray.get(0)); 475 | if (itemType.equals(subtype) == false) { // TODO recurse further 476 | arrayOK = false; 477 | } else { 478 | // make sure every item in the array is this type 479 | ArrayNode differentItems = JsonNodeFactory.instance.arrayNode(); 480 | for (Object val : argArray) { 481 | if (itemType.equals(getSymbol(val)) == false) { 482 | differentItems.add(expressionVisitor.visit((ExprListContext) val)); 483 | } 484 | } 485 | ; 486 | arrayOK = (differentItems.size() == 0); 487 | } 488 | } 489 | } 490 | } 491 | if (!arrayOK) { 492 | JsonNode type = s_arraySignatureMapping.get(subtype); 493 | if (type == null) { 494 | type = JsonNodeFactory.instance.nullNode(); 495 | } 496 | throw new EvaluateRuntimeException("Argument \"" + (argIndex + 1) + "\" of function \"" 497 | + functionName + "\" must be an array of \"" + type.asText() + "\""); 498 | } 499 | // the function expects an array. If it's not one, make it so 500 | if ("a".equals(single) == false) { 501 | ArrayNode wrappedArg = JsonNodeFactory.instance.arrayNode(); 502 | wrappedArg.add(arg); 503 | arg = wrappedArg; 504 | } 505 | } 506 | validatedArgs.add(arg); 507 | argIndex++; 508 | } else { 509 | validatedArgs.add(arg); 510 | argIndex++; 511 | } 512 | } 513 | } 514 | index++; 515 | } 516 | return validatedArgs; 517 | } 518 | throwValidationError(args, suppliedSig, functionName); 519 | // below just for the compiler as a runtime exception is thrown above 520 | return result; 521 | }; 522 | */ 523 | } 524 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.dashjoin.jsonata { 2 | requires java.compiler; 3 | requires java.management; 4 | 5 | exports com.dashjoin.jsonata; 6 | exports com.dashjoin.jsonata.json; 7 | exports com.dashjoin.jsonata.utils; 8 | } -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/.gitignore: -------------------------------------------------------------------------------- 1 | /gen 2 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/ArrayTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import static java.util.Arrays.asList; 5 | import static java.util.Map.of; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import java.util.Map; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class ArrayTest { 13 | 14 | @Test 15 | public void testArray() { 16 | Jsonata expr1 = jsonata("{'key': $append($.[{'x': 'y'}],$.[{'a': 'b'}])}"); 17 | var res1 = expr1.evaluate(of("key", asList(of("x", "y"), of("a", "b")))); 18 | Jsonata expr2 = jsonata("{'key': $append($.[{'x': 'y'}],[{'a': 'b'}])}"); 19 | var res2 = expr2.evaluate(of("key", asList(of("x", "y"), of("a", "b")))); 20 | assertEquals(res1, res2); 21 | } 22 | 23 | @Disabled 24 | @Test 25 | public void filterTest() { 26 | // Frame value not evaluated if used in array filter #45 27 | Jsonata expr = jsonata("($arr := [{'x':1}, {'x':2}];$arr[x=$number(variable.field)])"); 28 | Assertions.assertNotNull(expr.evaluate(Map.of("variable", Map.of("field", "1")))); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/CustomFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | import com.dashjoin.jsonata.Jsonata.JFunction; 9 | import com.dashjoin.jsonata.Jsonata.JFunctionCallable; 10 | 11 | public class CustomFunctionTest { 12 | 13 | @Test 14 | public void testSupplier() { 15 | var expression = Jsonata.jsonata("$greet()"); 16 | expression.registerFunction("greet", () -> "Hello world"); 17 | Assertions.assertEquals("Hello world", expression.evaluate(null)); 18 | } 19 | 20 | @Test 21 | public void testEval() { 22 | var expression = Jsonata.jsonata("$eval('$greet()')"); 23 | expression.registerFunction("greet", () -> "Hello world"); 24 | Assertions.assertEquals("Hello world", expression.evaluate(null)); 25 | } 26 | 27 | @Test 28 | public void testEvalWithParams() { 29 | var expression = Jsonata.jsonata("($eval('$greet()'))"); 30 | expression.registerFunction("greet", () -> "Hello world"); 31 | Assertions.assertEquals("Hello world", expression.evaluate(null)); 32 | } 33 | 34 | @Test 35 | public void testUnary() { 36 | var expression = Jsonata.jsonata("$echo(123)"); 37 | expression.registerFunction("echo", (x) -> x); 38 | Assertions.assertEquals(123, expression.evaluate(null)); 39 | } 40 | 41 | @Test 42 | public void testBinary() { 43 | var expression = Jsonata.jsonata("$add(21, 21)"); 44 | expression.registerFunction("add", (Integer a, Integer b) -> a + b); 45 | Assertions.assertEquals(42, expression.evaluate(null)); 46 | } 47 | 48 | @Test 49 | public void testTernary() { 50 | var expression = Jsonata.jsonata("$abc(a,b,c)"); 51 | expression.registerFunction("abc", new JFunction(new JFunctionCallable() { 52 | @SuppressWarnings("rawtypes") 53 | @Override 54 | public Object call(Object input, List args) throws Throwable { 55 | return (String) args.get(0) + (String) args.get(1) + (String) args.get(2); 56 | } 57 | }, "")); 58 | Assertions.assertEquals("abc", expression.evaluate(Map.of("a", "a", "b", "b", "c", "c"))); 59 | } 60 | 61 | /** 62 | * Lambdas use no signature - in case of an error, a ClassCastException is thrown 63 | */ 64 | @Test 65 | public void testLambdaSignatureError() { 66 | var expression = Jsonata.jsonata("$append(1, 2)"); 67 | expression.registerFunction("append", (Integer a, Boolean b) -> "" + a + b); 68 | Assertions.assertThrowsExactly(ClassCastException.class, () -> expression.evaluate(null)); 69 | } 70 | 71 | /** 72 | * provide signature: number, boolean => string 73 | */ 74 | @Test 75 | public void testJFunctionSignatureError() { 76 | var expression = Jsonata.jsonata("$append(1, 2)"); 77 | expression.registerFunction("append", new JFunction(new JFunctionCallable() { 78 | @Override 79 | public Object call(Object input, @SuppressWarnings("rawtypes") List args) throws Throwable { 80 | return "" + args.get(0) + args.get(1); 81 | } 82 | }, "")); 83 | JException ex = 84 | Assertions.assertThrowsExactly(JException.class, () -> expression.evaluate(null)); 85 | Assertions.assertEquals("T0410", ex.getError()); 86 | Assertions.assertEquals("append", ex.getExpected()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/DateTimeTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class DateTimeTest { 7 | @Test 8 | public void testFormatInteger() { 9 | Jsonata expr = Jsonata.jsonata("$toMillis('2018th', '[Y0001;o]')"); 10 | Assertions.assertEquals(1514764800000L, expr.evaluate(null)); 11 | } 12 | 13 | @Test 14 | public void testToMillis() { 15 | String noZoneTooPrecise = "2024-08-27T22:43:15.78133"; 16 | Jsonata expr = Jsonata.jsonata("$fromMillis($toMillis($))"); 17 | String timestamp = (String) expr.evaluate(noZoneTooPrecise); 18 | Assertions.assertTrue(timestamp.startsWith("2024-08-2")); 19 | Assertions.assertTrue(timestamp.endsWith(":43:15.781Z")); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/Generate.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Generate { 12 | 13 | public static void main(String[] args) throws IOException { 14 | 15 | boolean verbose = args.length>0 && args[0].startsWith("-v"); 16 | 17 | new File("src/test/java/com/dashjoin/jsonata/gen").mkdirs(); 18 | File suites = new File("jsonata/test/test-suite/groups"); 19 | int total = 0, testSuites = 0; 20 | File[] listSuites = suites.listFiles(); 21 | Arrays.sort(listSuites); 22 | for (File suite : listSuites) { 23 | 24 | StringBuffer b = new StringBuffer(); 25 | b.append("package com.dashjoin.jsonata.gen;\n"); 26 | b.append("import org.junit.jupiter.api.Test;\n"); 27 | b.append("import com.dashjoin.jsonata.JsonataTest;\n"); 28 | b.append("public class " + suite.getName().replace('-', '_') + "Test {\n"); 29 | 30 | File[] cases = suite.listFiles(); 31 | Arrays.sort(cases); 32 | int count = 0; 33 | for (File cas : cases) { 34 | // Skip all non-JSON 35 | if (!cas.getName().endsWith(".json")) continue; 36 | 37 | String name = cas.getName().substring(0, cas.getName().length() - 5); 38 | String jname = name.replace('-', '_'); 39 | 40 | Object jsonCase = new JsonataTest().readJson(cas.getAbsolutePath()); 41 | if (jsonCase instanceof List) { 42 | for (int i=0; i<((List)jsonCase).size(); i++) { 43 | b.append("// " + s(((Map)((List) jsonCase).get(i)).get("expr"))+"\n"); 44 | b.append("@Test public void " + jname.replace('.', '_') + "_case_"+i+ "() throws Exception { \n"); 45 | b.append(" new JsonataTest().runSubCase(\"jsonata/test/test-suite/groups/" + suite.getName() 46 | + "/" + name + ".json\", "+i+");\n"); 47 | b.append("}\n"); 48 | count++; total++; 49 | } 50 | } 51 | else { 52 | b.append("// " + s(((Map) jsonCase).get("expr"))+"\n"); 53 | b.append("@Test public void " + jname.replace('.', '_') + "() throws Exception { \n"); 54 | b.append(" new JsonataTest().runCase(\"jsonata/test/test-suite/groups/" + suite.getName() 55 | + "/" + name + ".json\");\n"); 56 | b.append("}\n"); 57 | count++; total++; 58 | } 59 | } 60 | b.append("}\n"); 61 | Files.write(Path.of("src/test/java/com/dashjoin/jsonata/gen/" 62 | + suite.getName().replace('-', '_') + "Test.java"), b.toString().getBytes()); 63 | if (verbose) 64 | System.out.println(b); 65 | System.out.println("Generated suite '"+suite.getName()+"' tests=" + count); 66 | testSuites++; 67 | } 68 | System.out.println("Generated SUITES="+testSuites+" TOTAL="+total); 69 | } 70 | 71 | static String s(Object o) { 72 | if (o == null) 73 | return null; 74 | String s = (String)o; 75 | return s.replace('\n', ' ').replace("\\u", "u"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/JsonataTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.nio.charset.Charset; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Map.Entry; 13 | 14 | import org.apache.commons.io.IOUtils; 15 | import org.junit.jupiter.api.Disabled; 16 | import org.junit.jupiter.api.Test; 17 | 18 | import static com.dashjoin.jsonata.Jsonata.jsonata; 19 | import com.dashjoin.jsonata.Jsonata.Frame; 20 | import com.dashjoin.jsonata.json.Json; 21 | import com.fasterxml.jackson.core.JsonProcessingException; 22 | import com.fasterxml.jackson.core.exc.StreamReadException; 23 | import com.fasterxml.jackson.databind.DatabindException; 24 | import com.fasterxml.jackson.databind.DeserializationFeature; 25 | import com.fasterxml.jackson.databind.JsonMappingException; 26 | import com.fasterxml.jackson.databind.ObjectMapper; 27 | 28 | import static com.dashjoin.jsonata.Jsonata.NULL_VALUE; 29 | 30 | @SuppressWarnings({"rawtypes", "unchecked"}) 31 | public class JsonataTest { 32 | 33 | boolean testExpr(String expr, Object data, Map bindings, 34 | Object expected, String code) { 35 | boolean success = true; 36 | try { 37 | 38 | if (debug) System.out.println("Expr="+expr+" Expected="+expected+" ErrorCode="+code); 39 | if (debug) System.out.println(data); 40 | 41 | Frame bindingFrame = null; 42 | if (bindings!=null) { 43 | // If we have bindings, create a binding env with the settings 44 | bindingFrame = new Frame(null); 45 | for (Entry e : bindings.entrySet()) { 46 | bindingFrame.bind(e.getKey(), e.getValue()); 47 | } 48 | } 49 | 50 | Jsonata jsonata = jsonata(expr); 51 | if (bindingFrame!=null) 52 | bindingFrame.setRuntimeBounds(debug ? 500000L : 1000L, 303); 53 | Object result = jsonata.evaluate(data, bindingFrame); 54 | if (code!=null) 55 | success = false; 56 | 57 | if (expected!=null && !expected.equals(result)) { 58 | // if ((""+expected).equals(""+result)) 59 | // System.out.println("Value equals failed, stringified equals = true. Result = "+result); 60 | // else 61 | success = false; 62 | } 63 | 64 | if (expected==null && result!=null) 65 | success = false; 66 | 67 | if (debug && success) System.out.println("--Result = "+result); 68 | 69 | if (!success) { 70 | System.out.println("--Expr="+expr+" Expected="+expected+" ErrorCode="+code); 71 | System.out.println("--Data="+data); 72 | System.out.println("--Result = "+result+" Class="+(result!=null ? result.getClass():null)); 73 | System.out.println("--Expect = "+expected+" ExpectedError="+code); 74 | System.out.println("WRONG RESULT"); 75 | } 76 | 77 | //assertEquals("Must be equal", expected, ""+result); 78 | } catch (Throwable t) { 79 | if (code==null) { 80 | System.out.println("--Expr="+expr+" Expected="+expected+" ErrorCode="+code); 81 | System.out.println("--Data="+data); 82 | 83 | if (t instanceof JException) { 84 | JException je = (JException)t; 85 | System.out.println("--Exception = "+je.error+" --> "+je); 86 | } else 87 | System.out.println("--Exception = "+t); 88 | 89 | System.out.println("--ExpectedError = "+code+" Expected="+expected); 90 | System.out.println("WRONG RESULT (exception)"); 91 | success = false; 92 | } 93 | if (!success) t.printStackTrace(System.out); 94 | if (debug && success) System.out.println("--Exception = "+t); 95 | //if (true) System.exit(-1); 96 | } 97 | return success; 98 | } 99 | 100 | static ObjectMapper om = new ObjectMapper().configure(DeserializationFeature.USE_LONG_FOR_INTS, true); 101 | ObjectMapper getObjectMapper() { 102 | return om; 103 | } 104 | 105 | Object toJson(String jsonStr) throws JsonMappingException, JsonProcessingException { 106 | //ObjectMapper om = getObjectMapper(); 107 | //Object json = om.readValue(jsonStr, Object.class); 108 | Object json = Json.parseJson(jsonStr); 109 | return json; 110 | } 111 | 112 | Object readJson(String name) throws StreamReadException, DatabindException, IOException { 113 | //ObjectMapper om = getObjectMapper(); 114 | //Object json = om.readValue(new java.io.FileReader(name, Charset.forName("UTF-8")), Object.class); 115 | 116 | Object json = Json.parseJson(new java.io.FileReader(name, Charset.forName("UTF-8"))); 117 | return json; 118 | } 119 | 120 | @Test 121 | public void testSimple() { 122 | testExpr("42", null, null, 42,null); 123 | testExpr("(3*(4-2)+1.01e2)/-2", null, null, -53.5,null); 124 | } 125 | 126 | @Test 127 | public void testPath() throws Exception { 128 | Object data = readJson("jsonata/test/test-suite/datasets/dataset0.json"); 129 | System.out.println(data); 130 | testExpr("foo.bar", data, null, 42,null); 131 | } 132 | 133 | static class TestDef { 134 | String expr; 135 | String dataset; 136 | Object bindings; 137 | Object result; 138 | } 139 | 140 | int testFiles = 0; 141 | int testCases = 0; 142 | 143 | public void runCase(String name) throws Exception { 144 | if (!runTestSuite(name)) 145 | throw new Exception(); 146 | } 147 | 148 | public void runSubCase(String name, int subNr) throws Exception { 149 | List cases = (List)readJson(name); 150 | if (!runTestCase(name+"_"+subNr, (Map) cases.get(subNr))) 151 | throw new Exception(); 152 | } 153 | 154 | boolean runTestSuite(String name) throws Exception { 155 | 156 | //System.out.println("Running test "+name); 157 | testFiles++; 158 | 159 | boolean success = true; 160 | 161 | Object testCase = readJson(name); 162 | if (testCase instanceof List) { 163 | // some cases contain a list of test cases 164 | // loop over the case definitions 165 | for (Object testDef : ((List)testCase)) { 166 | System.out.println("Running sub-test"); 167 | success &= runTestCase(name, (Map) testDef); 168 | } 169 | } else { 170 | success &= runTestCase(name, (Map) testCase); 171 | } 172 | return success; 173 | } 174 | 175 | void replaceNulls(Object o) { 176 | if (o instanceof List) { 177 | int index = 0; 178 | for (Object i : ((List) o)) { 179 | if (i == null) 180 | ((List) o).set(index, Jsonata.NULL_VALUE); 181 | else 182 | replaceNulls(i); 183 | index++; 184 | } 185 | } 186 | if (o instanceof Map) { 187 | for (Entry e : ((Map) o).entrySet()) { 188 | if (e.getValue() == null) 189 | e.setValue(Jsonata.NULL_VALUE); 190 | else 191 | replaceNulls(e.getValue()); 192 | } 193 | } 194 | } 195 | 196 | public static class TestOverride { 197 | public String name; 198 | public Boolean ignoreError; 199 | public Object alternateResult; 200 | public String alternateCode; 201 | public String reason; 202 | } 203 | 204 | public static class TestOverrides { 205 | public TestOverride[] override; 206 | } 207 | 208 | static TestOverrides testOverrides; 209 | 210 | static TestOverrides getTestOverrides() { 211 | if (testOverrides!=null) 212 | return testOverrides; 213 | 214 | try { 215 | testOverrides = new ObjectMapper().readValue( 216 | new File("test/test-overrides.json"), TestOverrides.class); 217 | } catch (IOException e) { 218 | e.printStackTrace(); 219 | throw new RuntimeException(e); 220 | } 221 | return testOverrides; 222 | } 223 | 224 | TestOverride getOverrideForTest(String name) { 225 | if (ignoreOverrides) return null; 226 | 227 | TestOverrides tos = getTestOverrides(); 228 | for (TestOverride to : tos.override) { 229 | if (name.indexOf(to.name)>=0) 230 | return to; 231 | } 232 | return null; 233 | } 234 | 235 | boolean runTestCase(String name, Map testDef) throws Exception { 236 | 237 | testCases++; 238 | if (debug) System.out.println("\nRunning test "+name); 239 | 240 | String expr = (String)testDef.get("expr"); 241 | 242 | if (expr==null) { 243 | String exprFile = (String)testDef.get("expr-file"); 244 | String fileName = name.substring(0, name.lastIndexOf("/")) + "/" + exprFile; 245 | expr = IOUtils.toString(new FileInputStream(fileName)); 246 | } 247 | 248 | String dataset = (String)testDef.get("dataset"); 249 | Map bindings = (Map)testDef.get("bindings"); 250 | Object result = testDef.get("result"); 251 | 252 | // if (result == null) 253 | // if (testDef.containsKey("result")) 254 | // result = Jsonata.NULL_VALUE; 255 | 256 | //replaceNulls(result); 257 | 258 | String code = (String)testDef.get("code"); 259 | 260 | if (testDef.get("error") instanceof Map) 261 | code = (String) ((Map)testDef.get("error")).get("code"); 262 | 263 | //System.out.println(""+bindings); 264 | 265 | Object data = testDef.get("data"); 266 | if (data==null && dataset!=null) 267 | data = readJson("jsonata/test/test-suite/datasets/"+dataset+".json"); 268 | 269 | TestOverride to = getOverrideForTest(name); 270 | if (to!=null) { 271 | System.out.println("OVERRIDE used : "+to.name+" for "+name+" reason="+to.reason); 272 | if (to.alternateResult!=null) { 273 | result = to.alternateResult; 274 | } 275 | if (to.alternateCode!=null) { 276 | code = to.alternateCode; 277 | } 278 | } 279 | boolean res; 280 | if (debug && expr.equals("( $inf := function(){$inf()}; $inf())")) { 281 | System.err.println("DEBUG MODE: skipping infinity test: "+expr); 282 | res = true; 283 | } 284 | else 285 | res = testExpr(expr, data, bindings, result, code); 286 | 287 | if (to!=null) { 288 | // There is an override/alternate result for this defined... 289 | if (res==false && to.ignoreError!=null && to.ignoreError) { 290 | System.out.println("Test "+name+" failed, but override allows failure"); 291 | res = true; 292 | } 293 | } 294 | 295 | return res; 296 | } 297 | 298 | String groupDir = "jsonata/test/test-suite/groups/"; 299 | 300 | boolean runTestGroup(String group) throws Exception { 301 | 302 | File dir = new File(groupDir, group); 303 | System.out.println("Run group "+dir); 304 | File[] files = dir.listFiles(); 305 | Arrays.sort(files); 306 | boolean success = true; 307 | int count = 0, good = 0; 308 | for (File f : files) { 309 | String name = f.getName(); 310 | if (name.endsWith(".json")) { 311 | boolean res = runTestSuite(groupDir+group+"/"+name); 312 | success &= res; 313 | 314 | count++; 315 | if (res) 316 | good++; 317 | } 318 | } 319 | int successPercentage = 100*good/count; 320 | System.out.println("Success: "+good+" / "+count+" = "+(100*good/count)+"%"); 321 | assertEquals(count, good, successPercentage+"% succeeded"); 322 | //assertEquals("100% test runs must succeed", 100, successPercentage); 323 | return success; 324 | } 325 | 326 | boolean debug = java.lang.management.ManagementFactory.getRuntimeMXBean(). 327 | getInputArguments().toString().contains("-Xrunjdwp:transport"); 328 | 329 | boolean ignoreOverrides = false; 330 | 331 | // For local dev: @Test 332 | public void testSuite() throws Exception { 333 | //runTestSuite("jsonata/test/test-suite/groups/boolean-expresssions/test.jsonx"); 334 | //runTestSuite("jsonata/test/test-suite/groups/boolean-expresssions/case017.json"); 335 | //runTestSuite("jsonata/test/test-suite/groups/fields/case000.json"); 336 | //runTestGroup("fields"); 337 | //runTestGroup("comments"); 338 | //runTestGroup("comparison-operators"); 339 | //runTestGroup("boolean-expresssions"); 340 | //runTestGroup("array-constructor"); 341 | //runTestGroup("transform"); 342 | //runTestGroup("function-substring"); 343 | //runTestGroup("wildcards"); 344 | //runTestSuite("jsonata/test/test-suite/groups/function-substring/case012.json"); 345 | //runTestSuite("jsonata/test/test-suite/groups/transform/case030.json"); 346 | //runTestSuite("jsonata/test/test-suite/groups/array-constructor/case006.json"); 347 | // Filter: 348 | //runTestSuite("jsonata/test/test-suite/groups/array-constructor/case017.json"); 349 | String s = "jsonata/test/test-suite/groups/wildcards/case003.json"; 350 | s = "jsonata/test/test-suite/groups/flattening/large.json"; 351 | s = "jsonata/test/test-suite/groups/function-sum/case006.json"; 352 | s = "jsonata/test/test-suite/groups/function-substring/case016.json"; 353 | s = "jsonata/test/test-suite/groups/null/case001.json"; 354 | s = "jsonata/test/test-suite/groups/context/case003.json"; 355 | s = "jsonata/test/test-suite/groups/object-constructor/case008.json"; 356 | runTestSuite(s); 357 | //String g = "function-applications"; // partly 358 | //String g = "higher-order-functions"; // works! 359 | //String g = "hof-map"; 360 | //String g = "joins"; 361 | //String g = "function-join"; // looks good 362 | //String g = "descendent-operator"; // nearly 363 | //String g = "object-constructor"; 364 | //String g = "flattening"; 365 | //String g = "parent-operator"; 366 | //String g = "function-substring"; // nearly - unicode encoding issues 367 | //String g = "function-substringBefore"; // works! 368 | //String g = "function-substringAfter"; // works! 369 | //String g = "function-sum"; // works! rounding error delta 370 | //String g = "function-max"; // nearly - [-1,-5] second unary wrong!!! 371 | //String g = "function-average"; // nearly - [-1,-5] second unary wrong!!! 372 | //String g = "function-pad"; // nearly - unicode 373 | //String g = "function-trim"; // works! 374 | //String g = "function-contains"; // works NO regexp 375 | //String g = "function-join"; // works NO regexp 376 | //runTestGroup(g); 377 | 378 | //runAllTestGroups(); 379 | } 380 | 381 | void runAllTestGroups() throws Exception { 382 | File dir = new File(groupDir); 383 | File[] groups = dir.listFiles(); 384 | Arrays.sort(groups); 385 | for (File g : groups) { 386 | String name = g.getName(); 387 | System.out.println("@Test"); 388 | System.out.println("public void runTestGroup_"+name.replaceAll("-","_")+"() {"); 389 | System.out.println("\trunTestGroup(\""+name+"\");"); 390 | System.out.println("}"); 391 | //runTestGroup(name); 392 | } 393 | 394 | System.out.println("Total test files="+testFiles+" cases="+testCases); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/NullSafetyTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.util.Arrays; 5 | import java.util.Map; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class NullSafetyTest { 10 | @Test 11 | public void testNullSafety() { 12 | Object res; 13 | res = jsonata("$sift(undefined, $uppercase)").evaluate(null); 14 | Assertions.assertEquals(null, res); 15 | 16 | res = jsonata("$each(undefined, $uppercase)").evaluate(null); 17 | Assertions.assertEquals(null, res); 18 | 19 | res = jsonata("$keys(null)").evaluate(null); 20 | Assertions.assertEquals(null, res); 21 | 22 | res = jsonata("$map(null, $uppercase)").evaluate(null); 23 | Assertions.assertEquals(null, res); 24 | 25 | res = jsonata("$filter(null, $uppercase)").evaluate(null); 26 | Assertions.assertEquals(null, res); 27 | 28 | res = jsonata("$single(null, $uppercase)").evaluate(null); 29 | Assertions.assertEquals(null, res); 30 | 31 | res = jsonata("$reduce(null, $uppercase)").evaluate(null); 32 | Assertions.assertEquals(null, res); 33 | 34 | res = jsonata("$lookup(null, 'anykey')").evaluate(null); 35 | Assertions.assertEquals(null, res); 36 | 37 | res = jsonata("$spread(null)").evaluate(null); 38 | Assertions.assertEquals(null, res); 39 | } 40 | 41 | @Test 42 | public void testFilterNull() { 43 | var x = Jsonata.jsonata("$filter($, function($v, $i, $a){$v})").evaluate(Arrays.asList(1, null)); 44 | Assertions.assertEquals(1, x); 45 | } 46 | 47 | @Test 48 | public void testNotNull() { 49 | Assertions.assertNull(Jsonata.jsonata("$not($)").evaluate(null)); 50 | } 51 | 52 | @Test 53 | public void testSingleNull() { 54 | var x = Jsonata.jsonata("$single($, function($v, $i, $a){ $v })").evaluate(Arrays.asList(null, 1)); 55 | Assertions.assertEquals(1, x); 56 | } 57 | 58 | @Test 59 | public void testFilterNullLookup() { 60 | var x = Jsonata.jsonata("$filter($, function($v, $i, $a){$lookup($v, 'content')})").evaluate( 61 | Arrays.asList(Map.of("content", "some"), Map.of())); 62 | Assertions.assertEquals(Map.of("content", "some"), x); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/NumberTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import static java.util.Map.of; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | import com.dashjoin.jsonata.json.Json; 9 | 10 | public class NumberTest { 11 | 12 | /** 13 | * this case fails, because the double value 1.0 is "untouched" 14 | */ 15 | @Disabled 16 | @Test 17 | public void testDouble() { 18 | Jsonata expr1 = jsonata("x"); 19 | var res = expr1.evaluate(of("x", 1.0)); 20 | assertEquals(1, res); 21 | } 22 | 23 | /** 24 | * a computation is applied, and com.dashjoin.jsonata.Utils.convertNumber(Number) casts the double to int 25 | */ 26 | @Test 27 | public void testDouble2() { 28 | Jsonata expr1 = jsonata("x+0"); 29 | var res = expr1.evaluate(of("x", 1.0)); 30 | assertEquals(1, res); 31 | } 32 | 33 | /** 34 | * here, the JSON parser immediately converts double 1.0 to int 1 35 | */ 36 | @Test 37 | public void testDouble3() { 38 | Jsonata expr1 = jsonata("x"); 39 | var res = expr1.evaluate(Json.parseJson("{\"x\":1.0}")); 40 | assertEquals(1, res); 41 | } 42 | 43 | /** 44 | * "clean" the input using com.dashjoin.jsonata.Utils.convertNumber(Number) 45 | */ 46 | @Test 47 | public void testDouble4() { 48 | Jsonata expr1 = jsonata("x"); 49 | var res = expr1.evaluate(of("x", Utils.convertNumber(1.0))); 50 | assertEquals(1, res); 51 | } 52 | 53 | /** 54 | * int 1 is converted to double when divided by 2 55 | */ 56 | @Test 57 | public void testInt() { 58 | Jsonata expr1 = jsonata("$ / 2"); 59 | var res = expr1.evaluate(1); 60 | assertEquals(0.5, res); 61 | } 62 | 63 | /** 64 | * JSONata constant 1.0 evaluates to 1 65 | */ 66 | @Test 67 | public void testConst() { 68 | Jsonata expr1 = jsonata("1.0"); 69 | var res = expr1.evaluate(null); 70 | assertEquals(1, res); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/ParseIntegerTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import static java.util.Arrays.asList; 5 | import static java.util.Map.of; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class ParseIntegerTest { 13 | 14 | @Test 15 | public void parseIntegerNoError() { 16 | Jsonata expr = jsonata("$parseInteger('xyz','000')"); 17 | var res = expr.evaluate(null); 18 | assertEquals(res, null); 19 | } 20 | 21 | @Disabled 22 | @Test 23 | public void parseIntegerError() { 24 | // The following test throws an error in the jsonata-js reference, 25 | // but DecimalFormat allows this format (plan is not to fix): 26 | assertThrows(Exception.class, () -> { 27 | Jsonata expr = jsonata("$parseInteger('000','xyz')"); 28 | var res = expr.evaluate(null); 29 | assertEquals(res, null); 30 | } ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/SerializationTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.ObjectInputStream; 8 | import java.io.ObjectOutputStream; 9 | import java.io.Serializable; 10 | import java.util.List; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | import com.dashjoin.jsonata.Jsonata.JFunction; 14 | import com.dashjoin.jsonata.Jsonata.JFunctionCallable; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | 17 | public class SerializationTest { 18 | 19 | @Test 20 | public void testJFunction() throws Exception { 21 | // return the function and test its serialization 22 | Jsonata expr = jsonata("$foo"); 23 | expr.registerFunction("foo", new JFunction(new JFunctionCallable() { 24 | 25 | @SuppressWarnings("rawtypes") 26 | @Override 27 | public Object call(Object input, List args) throws Throwable { 28 | return null; 29 | } 30 | 31 | }, null)); 32 | ObjectMapper om = new ObjectMapper(); 33 | System.out.println(expr.evaluate(null).getClass()); 34 | om.writeValueAsString(expr.evaluate(null)); 35 | } 36 | 37 | /** 38 | * wrapper class that makes Jsonata serializable 39 | */ 40 | public static class SerializableExpression implements Serializable { 41 | 42 | /** 43 | * jsonata expression 44 | */ 45 | public String expression; 46 | 47 | /** 48 | * parsed / transient expression 49 | */ 50 | transient public Jsonata jsonata; 51 | 52 | /** 53 | * constructor calls init 54 | */ 55 | public SerializableExpression(String expression) { 56 | init(expression); 57 | } 58 | 59 | /** 60 | * init the object before calling evaluate 61 | */ 62 | public void init(String expression) { 63 | // remember jsonata string 64 | this.expression = expression; 65 | 66 | // parse expression 67 | jsonata = jsonata(expression); 68 | 69 | // register any custom functions 70 | jsonata.registerFunction("hi", () -> "hello world"); 71 | } 72 | 73 | private static final long serialVersionUID = 7675531659407424684L; 74 | 75 | /** 76 | * custom serializer 77 | */ 78 | private void writeObject(java.io.ObjectOutputStream out) throws IOException { 79 | // only write jsonata string 80 | out.writeUTF(expression); 81 | } 82 | 83 | /** 84 | * custom deserializer 85 | */ 86 | private void readObject(java.io.ObjectInputStream in) 87 | throws IOException, ClassNotFoundException { 88 | // read jsonata string and init 89 | init(in.readUTF()); 90 | } 91 | } 92 | 93 | /** 94 | * test RMI / hazelcast serialization 95 | */ 96 | @Test 97 | public void testSerializable() throws Exception { 98 | // sample expression with custom function 99 | SerializableExpression expr = new SerializableExpression("$hi() & '!'"); 100 | 101 | // test output 102 | Assertions.assertEquals("hello world!", expr.jsonata.evaluate(null)); 103 | 104 | // buffer (i.e. network or file transport) 105 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 106 | 107 | // write to buffer 108 | ObjectOutputStream oos = new ObjectOutputStream(buffer); 109 | oos.writeObject(expr); 110 | 111 | // read from buffer 112 | ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); 113 | SerializableExpression clone = (SerializableExpression) ois.readObject(); 114 | 115 | // clone has same result 116 | Assertions.assertEquals(expr.jsonata.evaluate(null), clone.jsonata.evaluate(null)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/SignatureTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.util.List; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import com.dashjoin.jsonata.Jsonata.JFunction; 8 | import com.dashjoin.jsonata.Jsonata.JFunctionCallable; 9 | 10 | public class SignatureTest { 11 | 12 | @Test 13 | public void testParametersAreConvertedToArrays() { 14 | Jsonata expr = jsonata("$greet(1,null,3)"); 15 | expr.registerFunction("greet", new JFunction(new JFunctionCallable() { 16 | 17 | @Override 18 | public Object call(Object input, @SuppressWarnings("rawtypes") List args) throws Throwable { 19 | return args.toString(); 20 | } 21 | }, "")); 22 | Assertions.assertEquals("[[1], [null], [3], [null]]", expr.evaluate(null)); 23 | } 24 | 25 | @Test 26 | public void testError() { 27 | Jsonata expr = jsonata("$foo()"); 28 | expr.registerFunction("foo", new JFunction(new JFunctionCallable() { 29 | 30 | @Override 31 | public Object call(Object input, @SuppressWarnings("rawtypes") List args) throws Throwable { 32 | return null; 33 | } 34 | }, "(sao)")); 35 | 36 | // null not allowed 37 | Assertions.assertThrows(JException.class, ()->expr.evaluate(null)); 38 | 39 | // boolean not allowed 40 | Assertions.assertThrows(JException.class, ()->expr.evaluate(true)); 41 | } 42 | 43 | @Test 44 | public void testVarArg() { 45 | var expression = Jsonata.jsonata("$sumvar(1,2,3)"); 46 | expression.registerFunction("sumvar", new JFunction(new JFunctionCallable() { 47 | @SuppressWarnings("rawtypes") 48 | @Override 49 | public Object call(Object input, List args) throws Throwable { 50 | int sum = 0; 51 | for (Object i : args) 52 | sum += (int) i; 53 | return sum; 54 | } 55 | }, "")); 56 | Assertions.assertEquals(6, expression.evaluate(null)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/StringTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.util.Arrays; 5 | import java.util.Map; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * see https://docs.jsonata.org/string-functions#string 12 | */ 13 | public class StringTest { 14 | 15 | @Test 16 | public void stringTest() { 17 | Assertions.assertEquals("abc", jsonata("$string($)").evaluate("abc")); 18 | Assertions.assertEquals("100", jsonata("$string(100.0)").evaluate(null)); 19 | } 20 | 21 | @Disabled 22 | @Test 23 | public void stringExponentTest() { 24 | Assertions.assertEquals("100", jsonata("$string(x)").evaluate(Map.of("x", 100.0))); 25 | Assertions.assertEquals("100000000000000000000", jsonata("$string(x)").evaluate(Map.of("x", 100000000000000000000.0))); 26 | Assertions.assertEquals("1e+21", jsonata("$string(x)").evaluate(Map.of("x", 1000000000000000000000.0))); 27 | } 28 | 29 | @Test 30 | public void booleanTest() { 31 | Assertions.assertEquals("true", jsonata("$string($)").evaluate(true)); 32 | } 33 | 34 | @Test 35 | public void numberTest() { 36 | Assertions.assertEquals("5", jsonata("$string(5)").evaluate(null)); 37 | } 38 | 39 | @Test 40 | public void arrayTest() { 41 | Assertions.assertEquals(Arrays.asList("1", "2", "3", "4", "5"), 42 | jsonata("[1..5].$string()").evaluate(null)); 43 | } 44 | 45 | @Test 46 | public void mapTest() { 47 | Assertions.assertEquals("{}", jsonata("$string($)").evaluate(Map.of())); 48 | } 49 | 50 | @Test 51 | public void map2Test() { 52 | Assertions.assertEquals("{\"x\":1}", jsonata("$string($)").evaluate(Map.of("x", 1))); 53 | } 54 | 55 | @Test 56 | public void escapeTest() { 57 | Assertions.assertEquals("{\"a\":\"\\\"\"}", 58 | jsonata("$string($)").evaluate(Map.of("a", "" + '"'))); 59 | Assertions.assertEquals("{\"a\":\"\\\\\"}", 60 | jsonata("$string($)").evaluate(Map.of("a", "" + '\\'))); 61 | Assertions.assertEquals("{\"a\":\"\\t\"}", 62 | jsonata("$string($)").evaluate(Map.of("a", "" + '\t'))); 63 | Assertions.assertEquals("{\"a\":\"\\n\"}", 64 | jsonata("$string($)").evaluate(Map.of("a", "" + '\n'))); 65 | Assertions.assertEquals("{\"a\":\" $split('-')").evaluate(Map.of()); 82 | Assertions.assertNull(res); 83 | 84 | // Splitting empty string with empty separator must return empty list 85 | res = jsonata("$split('', '')").evaluate(null); 86 | Assertions.assertEquals(Arrays.asList(), res); 87 | 88 | // Split characters with limit 89 | res = jsonata("$split('a1b2c3d4', '', 4)").evaluate(null); 90 | Assertions.assertEquals(Arrays.asList("a", "1", "b", "2"), res); 91 | 92 | // Check string is not treated as regexp 93 | res = jsonata("$split('this..is.a.test', '.')").evaluate(null); 94 | //System.out.println( Functions.string(res, false)); 95 | Assertions.assertEquals(Arrays.asList("this","","is","a","test"), res); 96 | 97 | // Check trailing empty strings 98 | res = jsonata("$split('this..is.a.test...', '.')").evaluate(null); 99 | //System.out.println( Functions.string(res, false)); 100 | Assertions.assertEquals(Arrays.asList("this","","is","a","test","","",""), res); 101 | 102 | // Check trailing empty strings 103 | res = jsonata("$split('this..is.a.test...', /\\./)").evaluate(null); 104 | Assertions.assertEquals(Arrays.asList("this","","is","a","test","","",""), res); 105 | 106 | // Check string is not treated as regexp, trailing empty strings, and limit 107 | res = jsonata("$split('this.*.*is.*a.*test.*.*.*.*.*.*', '.*', 8)").evaluate(null); 108 | Assertions.assertEquals(Arrays.asList("this","","is","a","test","","",""), res); 109 | 110 | // Escaped regexp, trailing empty strings, and limit 111 | res = jsonata("$split('this.*.*is.*a.*test.*.*.*.*.*.*', /\\.\\*/, 8)").evaluate(null); 112 | Assertions.assertEquals(Arrays.asList("this","","is","a","test","","",""), res); 113 | } 114 | 115 | @Test 116 | public void trimTest() { 117 | Assertions.assertEquals("", jsonata("$trim(\"\n\")").evaluate(null)); 118 | Assertions.assertEquals("", jsonata("$trim(\" \")").evaluate(null)); 119 | Assertions.assertEquals("", jsonata("$trim(\"\")").evaluate(null)); 120 | Assertions.assertEquals(null, jsonata("$trim(notthere)").evaluate(null)); 121 | } 122 | 123 | @Test 124 | public void evalTest() { 125 | Assertions.assertEquals("AAA", jsonata("(\n" 126 | + " $data := {'Wert1': 'AAA', 'Wert2': 'BBB'};\n" 127 | + " $eval('$data.Wert1')\n" 128 | + ")").evaluate(null)); 129 | } 130 | 131 | @Disabled 132 | @Test 133 | public void replaceTest() { 134 | Assertions.assertEquals("http://example.org/test", 135 | jsonata("$replace($, /{par}/, '')").evaluate("http://example.org/test{par}")); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/ThreadTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.util.Map; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.Future; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import com.dashjoin.jsonata.Jsonata.Frame; 11 | 12 | public class ThreadTest { 13 | 14 | @Test 15 | public void testReuse() { 16 | Jsonata expr = jsonata("a"); 17 | Assertions.assertEquals(1, expr.evaluate(Map.of("a", 1))); 18 | Assertions.assertEquals(1, expr.evaluate(Map.of("a", 1))); 19 | } 20 | 21 | @Test 22 | public void testNow() throws InterruptedException { 23 | Jsonata now = jsonata("$now()"); 24 | Object r1 = now.evaluate(null); 25 | Thread.sleep(42); 26 | Object r2 = now.evaluate(null); 27 | Assertions.assertNotEquals(r1, r2); 28 | } 29 | 30 | @Test 31 | public void testReuseWithVariable() throws InterruptedException, ExecutionException { 32 | Jsonata expr = jsonata("($x := a; $wait(a); $x)"); 33 | expr.registerFunction("wait", (Integer a) -> { 34 | try { 35 | Thread.sleep(a); 36 | } catch (InterruptedException e) { 37 | e.printStackTrace(); 38 | } 39 | return null; 40 | }); 41 | 42 | // start a thread that sets x=100 and waits 100 before returning x 43 | Future outer = 44 | Executors.newSingleThreadExecutor().submit(() -> expr.evaluate(Map.of("a", 100))); 45 | 46 | // make sure outer thread is initialized and in $wait 47 | Thread.sleep(10); 48 | 49 | // this thread uses the same expr and terminates before thread is done 50 | Assertions.assertEquals(30, expr.evaluate(Map.of("a", 30))); 51 | 52 | // the outer thread is unaffected by the previous operations 53 | Assertions.assertEquals(100, outer.get()); 54 | } 55 | 56 | @Test 57 | public void testAddEnvAndInput() throws Exception { 58 | Jsonata expr = jsonata("$eval('$count($keys($))')"); 59 | Map input1 = Map.of("input", 1); 60 | Map input2 = Map.of("input", 2, "other", 3); 61 | Frame frame1 = expr.createFrame(); 62 | Frame frame2 = expr.createFrame(); 63 | frame1.bind("variable", 1); 64 | frame2.bind("variable", 2); 65 | 66 | int count = 10000; 67 | Future out = Executors.newSingleThreadExecutor().submit(() -> { 68 | int sum = 0; 69 | for (int i = 0; i < count; i++) { 70 | sum += (int) expr.evaluate(input1); 71 | } 72 | return sum; 73 | }); 74 | 75 | int sum = 0; 76 | for (int i = 0; i < count; i++) { 77 | sum += (int) expr.evaluate(input2); 78 | } 79 | 80 | Assertions.assertEquals(count, out.get()); 81 | Assertions.assertEquals(2*count, sum); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/dashjoin/jsonata/TypesTest.java: -------------------------------------------------------------------------------- 1 | package com.dashjoin.jsonata; 2 | 3 | import static com.dashjoin.jsonata.Jsonata.jsonata; 4 | import java.math.BigDecimal; 5 | import java.util.Arrays; 6 | import java.util.Date; 7 | import java.util.Map; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | 12 | public class TypesTest { 13 | 14 | @Test 15 | public void testIllegalTypes() { 16 | // array 17 | Assertions.assertThrows(IllegalArgumentException.class, 18 | () -> jsonata("$").evaluate(new int[] {0, 1, 2, 3})); 19 | // char 20 | Assertions.assertThrows(IllegalArgumentException.class, () -> jsonata("$").evaluate('c')); 21 | // date 22 | Assertions.assertThrows(IllegalArgumentException.class, 23 | () -> jsonata("$").evaluate(new Date())); 24 | } 25 | 26 | @Test 27 | public void testLegalTypes() { 28 | // map 29 | Assertions.assertEquals(1, jsonata("a").evaluate(Map.of("a", 1))); 30 | // list 31 | Assertions.assertEquals(1, jsonata("$[0]").evaluate(Arrays.asList(1, 2))); 32 | // string 33 | Assertions.assertEquals("string", jsonata("$").evaluate("string")); 34 | // int 35 | Assertions.assertEquals(1, jsonata("$").evaluate(1)); 36 | // long 37 | Assertions.assertEquals(1L, jsonata("$").evaluate(1L)); 38 | // boolean 39 | Assertions.assertEquals(true, jsonata("$").evaluate(true)); 40 | // double 41 | Assertions.assertEquals(1.0, jsonata("$").evaluate(1.0)); 42 | // float 43 | Assertions.assertEquals((float) 1.0, jsonata("$").evaluate((float) 1.0)); 44 | // big decimal 45 | Assertions.assertEquals(new BigDecimal(3.14), jsonata("$").evaluate(new BigDecimal(3.14))); 46 | } 47 | 48 | public static class Pojo { 49 | public char c = 'c'; 50 | public Date d = new Date(); 51 | public int[] arr = new int[] {0, 1, 2, 3}; 52 | } 53 | 54 | @Test 55 | public void testJacksonConversion() { 56 | ObjectMapper om = new ObjectMapper(); 57 | Object input = om.convertValue(new Pojo(), Object.class); 58 | Assertions.assertEquals("c", jsonata("c").evaluate(input)); 59 | Assertions.assertEquals(0, jsonata("arr[0]").evaluate(input)); 60 | Assertions.assertEquals(Long.class, jsonata("d").evaluate(input).getClass()); 61 | 62 | Object output = jsonata("$").evaluate(input); 63 | Assertions.assertEquals('c', om.convertValue(output, Pojo.class).c); 64 | } 65 | 66 | @SuppressWarnings("unchecked") 67 | @Test 68 | public void testCustomFunction() { 69 | ObjectMapper om = new ObjectMapper(); 70 | Jsonata fn = jsonata("$foo()"); 71 | fn.registerFunction("foo", () -> om.convertValue(new Pojo(), Object.class)); 72 | Map res = (Map) fn.evaluate(null); 73 | Assertions.assertEquals("c", res.get("c")); 74 | } 75 | 76 | @Test 77 | public void testIgnore() { 78 | Jsonata expr = jsonata("a"); 79 | Date date = new Date(); 80 | 81 | // date causes exception 82 | Assertions.assertThrows(IllegalArgumentException.class, () -> expr.evaluate(Map.of("a", date))); 83 | 84 | // turn off validation, Date is "passed" via $ 85 | expr.setValidateInput(false); 86 | Assertions.assertEquals(date, expr.evaluate(Map.of("a", date))); 87 | 88 | // change expression to a computation that involves a, we get an error again because concat 89 | // cannot deal with Date 90 | Jsonata expr2 = jsonata("a & a"); 91 | expr2.setValidateInput(false); 92 | Assertions.assertThrows(IllegalArgumentException.class, 93 | () -> expr2.evaluate(Map.of("a", date))); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/test-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "override": [ 3 | { 4 | "name": "function-formatInteger/formatInteger.json_43", 5 | "ignoreError": true, 6 | "reason": "Do not support number to word for numbers exceeding the 64-bit range" 7 | }, 8 | { 9 | "name": "function-parseInteger/parseInteger.json_11", 10 | "ignoreError": true, 11 | "reason": "Cannot parse special chars" 12 | }, 13 | { 14 | "name": "function-parseInteger/parseInteger.json_12", 15 | "ignoreError": true, 16 | "reason": "Cannot parse special chars" 17 | }, 18 | { 19 | "name": "function-parseInteger/parseInteger.json_39", 20 | "ignoreError": true, 21 | "reason": "Implementation currently uses Long instead of BigDecimal" 22 | }, 23 | { 24 | "name": "function-formatNumber/case002.json", 25 | "ignoreError": true, 26 | "reason": "Only last part of formatNumber case is taken into account" 27 | }, 28 | { 29 | "name": "function-formatNumber/case003.json", 30 | "ignoreError": true, 31 | "reason": "Only last part of formatNumber case is taken into account" 32 | }, 33 | { 34 | "name": "function-formatNumber/case011.json", 35 | "ignoreError": true, 36 | "reason": "formatNumber strange chars not implemented" 37 | }, 38 | { 39 | "name": "function-formatNumber/case013.json", 40 | "ignoreError": true, 41 | "reason": "formatNumber exponent not implemented" 42 | }, 43 | { 44 | "name": "function-formatNumber/case014.json", 45 | "ignoreError": true, 46 | "reason": "formatNumber exponent not implemented" 47 | }, 48 | { 49 | "name": "function-formatNumber/case016.json", 50 | "ignoreError": true, 51 | "reason": "Only last part of formatNumber case is taken into account" 52 | }, 53 | { 54 | "name": "function-formatNumber/case025.json", 55 | "ignoreError": true, 56 | "reason": "formatNumber exponent not implemented" 57 | }, 58 | { 59 | "name": "function-fromMillis/isoWeekDate.json_0", 60 | "ignoreError": true, 61 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 62 | }, 63 | { 64 | "name": "function-fromMillis/isoWeekDate.json_1", 65 | "ignoreError": true, 66 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 67 | }, 68 | { 69 | "name": "function-fromMillis/isoWeekDate.json_3", 70 | "ignoreError": true, 71 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 72 | }, 73 | { 74 | "name": "function-fromMillis/isoWeekDate.json_8", 75 | "ignoreError": true, 76 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 77 | }, 78 | { 79 | "name": "function-fromMillis/isoWeekDate.json_11", 80 | "ignoreError": true, 81 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 82 | }, 83 | { 84 | "name": "function-fromMillis/isoWeekDate.json_12", 85 | "ignoreError": true, 86 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 87 | }, 88 | { 89 | "name": "function-fromMillis/isoWeekDate.json_13", 90 | "ignoreError": true, 91 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 92 | }, 93 | { 94 | "name": "function-fromMillis/isoWeekDate.json_16", 95 | "ignoreError": true, 96 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 97 | }, 98 | { 99 | "name": "function-fromMillis/isoWeekDate.json_17", 100 | "ignoreError": true, 101 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 102 | }, 103 | { 104 | "name": "function-fromMillis/isoWeekDate.json_18", 105 | "ignoreError": true, 106 | "reason": "Ignore all fromMillis ISO week date (non-standard picture string X0001) cases - [X] behaves like [Y]" 107 | }, 108 | { 109 | "name": "function-fromMillis/formatDateTime.json_61", 110 | "alternateResult": "Week: 0 of February", 111 | "reason": "Ignore formatDateTime cases + differences in Week 5 of Jan vs Week 0 of Feb" 112 | }, 113 | { 114 | "name": "function-fromMillis/formatDateTime.json_63", 115 | "alternateResult": "Week: 0 of January", 116 | "reason": "Ignore formatDateTime cases + differences in Week 5 of Jan vs Week 0 of Feb" 117 | }, 118 | { 119 | "name": "function-fromMillis/formatDateTime.json_64", 120 | "alternateResult": "Week: 5 of July", 121 | "reason": "Ignore formatDateTime cases + differences in Week 5 of Jan vs Week 0 of Feb" 122 | }, 123 | { 124 | "name": "function-fromMillis/formatDateTime.json_65", 125 | "alternateResult": "Week: 5 of December", 126 | "reason": "Ignore formatDateTime cases + differences in Week 5 of Jan vs Week 0 of Feb" 127 | }, 128 | { 129 | "name": "function-string/case006.json", 130 | "alternateResult": "1e+20", 131 | "reason": "toString precision should be 15, handled differently by Java Bigdecimal(..., new MathContext(15)" 132 | }, 133 | { 134 | "name": "function-sort/case009.json", 135 | "alternateResult": ["0406634348", "040657863", "0406654608", "0406654603"], 136 | "reason": "the sort expression is evaluated correctly - however the sort algorithm seems to behave slightly different for equality - jsonata expects the native order to be preserved" 137 | }, 138 | { 139 | "name": "function-sort/case010.json", 140 | "alternateResult": ["0406634348", "0406654608", "040657863", "0406654603"], 141 | "reason": "same as above, works if the condition is changed to >=" 142 | }, 143 | { 144 | "name": "function-applications/case008.json", 145 | "ignoreError": true, 146 | "reason": "Exception instead null/empty result. OK because the path has no match so NULL_VALUE is used as argument for $substringAfter" 147 | }, 148 | { 149 | "name": "matchers/case000.json", 150 | "ignoreError": true, 151 | "reason": "Custom matcher function not supported in Java regexp lib. OK because not much value seen" 152 | }, 153 | { 154 | "name": "regex/case022.json", 155 | "ignoreError": true, 156 | "reason": "Java regexp OK with this case. Jsonata regexp throws error because the regexp matches 0 characters, which could cause an endless loop in the custom matcher." 157 | } 158 | ] 159 | } 160 | --------------------------------------------------------------------------------