├── .github └── workflows │ ├── adocs-build.yml │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── docs ├── build_expressions.adoc ├── getting_started.adoc ├── images │ ├── hld.png │ └── sample-domain-model.png ├── include.adoc ├── index.adoc ├── introduction.adoc ├── public_api.adoc └── query_specs.adoc ├── logo.png ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── mhewedy │ └── expressions │ ├── DateTimeUtil.java │ ├── Expression.java │ ├── Expressions.java │ ├── ExpressionsPredicateBuilder.java │ ├── ExpressionsRepository.java │ ├── ExpressionsRepositoryImpl.java │ └── Operator.java └── test ├── java └── com │ └── github │ └── mhewedy │ └── expressions │ ├── BookRepository.java │ ├── EmployeeRepository.java │ ├── ExpressionsRepositoryImplTest.java │ ├── ExpressionsTest.java │ ├── HijrahDateConverter.java │ └── model │ ├── Auditable.java │ ├── Book.java │ ├── City.java │ ├── Department.java │ ├── Employee.java │ ├── LingualString.java │ ├── Status.java │ └── Task.java └── resources ├── application.yaml ├── testComplexCaseWithMultipleOrAndExpressions.json ├── testComplexCaseWithMultipleOrAndExpressions2.json ├── testCompositeIdUsingEmbeddable.json ├── testCompositeIdUsingEmbeddable_QueryByIdParts.json ├── testCompositeIdUsingEmbeddable_QueryByIdParts2.json ├── testEmbeddedAndJoin.json ├── testEmbeddedInAndJoin.json ├── testEnumInInts.json ├── testEnumNotInStrings.json ├── testFindAllInBaseRepository.json ├── testHijrahDate.json ├── testInvalidFieldName.json ├── testLeftJoin.json ├── testLeftJoinAlternateSyntax.json ├── testManyToOneIsNull.json ├── testNestingUsingManyToOneJoin.json ├── testNestingUsingManyToOneJoinUsingDeepNestedLevel.json ├── testNestingUsingManyToOneJoinUsingInQueries.json ├── testNestingUsingManyToOneJoinWithMultipleFields.json ├── testNestingUsingManyToOneJoinWithMultipleFields_Advanced.json ├── testNestingUsingOneToManyJoin.json ├── testNumberContains.json ├── testOperatorNotSupported.json ├── testPagingAndSorting.json ├── testSearchByContains.json ├── testSearchByIgnoreCaseContains.json ├── testSearchByNull.json ├── testSearchUsingInOperatorInNonIntegerNumericField.json ├── testUUID.json ├── testUsingTheBuilderMethodOfExpressionsWithOr.json └── testUsingTheBuilderMethodOfExpressionssWithAnd.json /.github/workflows/adocs-build.yml: -------------------------------------------------------------------------------- 1 | name: build adocs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | adoc_build: 9 | runs-on: ubuntu-latest 10 | name: asciidoctor -D docs --backend=html5 -o index.html -a toc2 docs/index.adoc 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Get build container 15 | id: adocbuild 16 | uses: tonynv/asciidoctor-action@master 17 | with: 18 | program: "asciidoctor -D docs --backend=html5 -o index.html docs/index.adoc" 19 | - name: Print execution time 20 | run: echo "Time ${{ steps.adocbuild.outputs.time }}" 21 | - name: Deploy docs to ghpages 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_branch: gh-pages 26 | publish_dir: ./docs 27 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | [push, pull_request] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: 17 20 | distribution: 'adopt' 21 | server-id: ossrh 22 | server-username: OSSRH_USERNAME 23 | server-password: OSSRH_PASSWORD 24 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 25 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 26 | - name: Build with Maven 27 | run: mvn -B compile --file pom.xml 28 | 29 | - name: Test with Maven 30 | run: mvn -B test --file pom.xml 31 | 32 | - name: Submit code coverage 33 | run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_SECRET }} 34 | 35 | - name: Publish to Apache Maven Central 36 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 37 | run: cat ~/.m2/settings.xml && mvn -DskipTests deploy -Dgpg.passphrase=${{ secrets.MAVEN_GPG_PASSPHRASE }} 38 | env: 39 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 40 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 41 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import java.net.*; 18 | import java.io.*; 19 | import java.nio.channels.*; 20 | import java.util.Properties; 21 | 22 | public class MavenWrapperDownloader { 23 | 24 | private static final String WRAPPER_VERSION = "0.5.6"; 25 | /** 26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 27 | */ 28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 30 | 31 | /** 32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 33 | * use instead of the default one. 34 | */ 35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 36 | ".mvn/wrapper/maven-wrapper.properties"; 37 | 38 | /** 39 | * Path where the maven-wrapper.jar will be saved to. 40 | */ 41 | private static final String MAVEN_WRAPPER_JAR_PATH = 42 | ".mvn/wrapper/maven-wrapper.jar"; 43 | 44 | /** 45 | * Name of the property which should be used to override the default download url for the wrapper. 46 | */ 47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 48 | 49 | public static void main(String args[]) { 50 | System.out.println("- Downloader started"); 51 | File baseDirectory = new File(args[0]); 52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 53 | 54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 55 | // wrapperUrl parameter. 56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 57 | String url = DEFAULT_DOWNLOAD_URL; 58 | if (mavenWrapperPropertyFile.exists()) { 59 | FileInputStream mavenWrapperPropertyFileInputStream = null; 60 | try { 61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 62 | Properties mavenWrapperProperties = new Properties(); 63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 65 | } catch (IOException e) { 66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 67 | } finally { 68 | try { 69 | if (mavenWrapperPropertyFileInputStream != null) { 70 | mavenWrapperPropertyFileInputStream.close(); 71 | } 72 | } catch (IOException e) { 73 | // Ignore ... 74 | } 75 | } 76 | } 77 | System.out.println("- Downloading from: " + url); 78 | 79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 80 | if (!outputFile.getParentFile().exists()) { 81 | if (!outputFile.getParentFile().mkdirs()) { 82 | System.out.println( 83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 84 | } 85 | } 86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 87 | try { 88 | downloadFileFromURL(url, outputFile); 89 | System.out.println("Done"); 90 | System.exit(0); 91 | } catch (Throwable e) { 92 | System.out.println("- Error downloading"); 93 | e.printStackTrace(); 94 | System.exit(1); 95 | } 96 | } 97 | 98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 100 | String username = System.getenv("MVNW_USERNAME"); 101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 102 | Authenticator.setDefault(new Authenticator() { 103 | @Override 104 | protected PasswordAuthentication getPasswordAuthentication() { 105 | return new PasswordAuthentication(username, password); 106 | } 107 | }); 108 | } 109 | URL website = new URL(urlString); 110 | ReadableByteChannel rbc; 111 | rbc = Channels.newChannel(website.openStream()); 112 | FileOutputStream fos = new FileOutputStream(destination); 113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 114 | fos.close(); 115 | rbc.close(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhewedy/spring-data-jpa-mongodb-expressions/d7a6c777d9874f31ebca84b785b607042c977eaf/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /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 | # Spring Data JPA MongoDB Expressions 2 | 3 | [![Java CI with Maven](https://github.com/mhewedy/spring-data-jpa-mongodb-expressions/actions/workflows/maven.yml/badge.svg)](https://github.com/mhewedy/spring-data-jpa-mongodb-expressions/actions/workflows/maven.yml) 4 | [![codecov](https://codecov.io/gh/mhewedy/spring-data-jpa-mongodb-expressions/branch/master/graph/badge.svg?token=3BR9MGYVC8)](https://codecov.io/gh/mhewedy/spring-data-jpa-mongodb-expressions) 5 | [![javadoc](https://javadoc.io/badge2/com.github.mhewedy/spring-data-jpa-mongodb-expressions/javadoc.svg)](https://javadoc.io/doc/com.github.mhewedy/spring-data-jpa-mongodb-expressions) 6 | [![Join the chat at https://gitter.im/spring-data-jpa-mongodb-expressions/community](https://badges.gitter.im/spring-data-jpa-mongodb-expressions/community.svg)](https://gitter.im/spring-data-jpa-mongodb-expressions/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Use%20the%20MongoDB%20query%20syntax%20to%20query%20your%20relational%20database&url=https://github.com/mhewedy/spring-data-jpa-mongodb-expressions&via=Github&hashtags=java,springboot,mongodb,jpa,hibernate) 8 | 9 | 10 | 11 | ### How it works: 12 | 13 | 1. Customize JPA Repository base class: 14 | ```java 15 | @SpringBootApplication 16 | @EnableJpaRepositories(repositoryBaseClass = ExpressionsRepositoryImpl.class) 17 | public class Application { … } 18 | ``` 19 | 2. Change your repository to extend `ExpressionsRepository`: 20 | ```java 21 | @Repository 22 | public interface EmployeeRepository extends ExpressionsRepository { 23 | } 24 | ``` 25 | 3. Build the controller/service: 26 | ```java 27 | @PostMapping("/search") 28 | public ResponseEntity> search(@RequestBody Expressions expressions, Pageable pageable) { 29 | 30 | return ok().body( 31 | employeeRepository.findAll(expressions, pageable).map(employeeMapper::toDto) 32 | ); 33 | } 34 | ``` 35 | 4. Send [Mongodb query in JSON](https://mhewedy.github.io/spring-data-jpa-mongodb-expressions/#_how_to_build_the_expressions) from frontend: 36 | ```json 37 | { 38 | "$or": [ 39 | {"lastName": "ibrahim"}, 40 | { 41 | "$and": [ 42 | {"firstName": "mostafa"}, 43 | {"birthDate": {"$gt": "1990-01-01"}} 44 | ] 45 | } 46 | ] 47 | } 48 | ``` 49 | 50 | ### Learn more 51 | 52 | For a quick start see [this Medium post](https://mohewedy.medium.com/using-mongodb-query-syntax-to-query-relational-database-in-java-57701f0b0f0) 53 | or [dev.to post](https://dev.to/mhewedy/using-mongodb-query-syntax-to-query-relational-database-in-java-49hf) 54 | or see [this demo example on Github](https://github.com/springexamples/spring-data-jpa-mongodb-expressions-demo). 55 | 56 | See [documentation website](https://mhewedy.github.io/spring-data-jpa-mongodb-expressions/) for details about how to get started. 57 | 58 | ### Install: 59 | 60 | For spring-boot 3.x: 61 | 62 | ```xml 63 | 64 | com.github.mhewedy 65 | spring-data-jpa-mongodb-expressions 66 | 0.1.9 67 | 68 | 69 | ``` 70 | For spring-boot 2.x: 71 | 72 | ```xml 73 | 74 | com.github.mhewedy 75 | spring-data-jpa-mongodb-expressions 76 | 0.0.8 77 | 78 | 79 | ``` 80 | 81 | #### 🎖 Special Thanks 82 | 83 | Special thanks to [Rashad Saif](https://github.com/rashadsaif) and [Hamada Elnoby](https://github.com/hamadaelnopy) for helping in the design, inspring with ideas, and for doing code review. 84 | 85 | #### In the News 86 | This repo has been mentioned in [spring.io](http://spring.io/blog/2021/07/06/this-week-in-spring-july-6th-2021) weekly news. 87 | -------------------------------------------------------------------------------- /docs/build_expressions.adoc: -------------------------------------------------------------------------------- 1 | = How to Build the Expressions 2 | include::include.adoc[] 3 | 4 | Building the expression on the frontend using `Javascript` is easy. 5 | 6 | .Example: 7 | 8 | [source,javascript] 9 | ---- 10 | const q = {}; 11 | 12 | q.status = 'A'; 13 | q.qty = { "$lt": 30 }; 14 | 15 | console.log(JSON.stringify(q, null, 4)); 16 | ---- 17 | 18 | Output: 19 | [source,json,javascript] 20 | ---- 21 | { 22 | "status": "A", 23 | "qty": { 24 | "$lt": 30 25 | } 26 | } 27 | ---- 28 | 29 | .Another example: 30 | 31 | [source,javascript] 32 | ---- 33 | let q = {}; 34 | 35 | q.lastName = "ibrahim"; 36 | q.$and = [ 37 | { birthDate: { $gt: "1981-01-03" } }, 38 | { birthDate: { $lte: "1981-01-03" } } 39 | ]; 40 | 41 | console.log(JSON.stringify(q, null, 4)); 42 | ---- 43 | 44 | Output: 45 | [source,json,javascript] 46 | ---- 47 | { 48 | "lastName": "ibrahim", 49 | "$and": [ 50 | { 51 | "birthDate": { 52 | "$gt": "1981-01-03" 53 | } 54 | }, 55 | { 56 | "birthDate": { 57 | "$lte": "1981-01-03" 58 | } 59 | } 60 | ] 61 | } 62 | ---- 63 | -------------------------------------------------------------------------------- /docs/getting_started.adoc: -------------------------------------------------------------------------------- 1 | = Getting Started 2 | include::include.adoc[] 3 | 4 | It's really easy to get started with *Spring Data JPA MongoDB Expressions*. This section shows you how. 5 | 6 | == Installation 7 | 8 | Add the following dependency in your Spring/Spring Boot project, where you already have 9 | https://start.spring.io/#!dependencies=data-jpa[__spring-data-jpa__,role=external,window=_blank] dependency added. 10 | 11 | === Maven 12 | [source,xml,subs="attributes+"] 13 | ---- 14 | 15 | com.github.mhewedy 16 | spring-data-jpa-mongodb-expressions 17 | {revnumber} 18 | 19 | ---- 20 | 21 | === Gradle 22 | [source,groovy,subs="attributes+"] 23 | ---- 24 | implementation 'com.github.mhewedy:spring-data-jpa-mongodb-expressions:{revnumber}' 25 | ---- 26 | 27 | IMPORTANT: *Spring Data JPA MongoDB Expressions* will not bring any dependencies with it - this is by design - so to avoid version overlap 28 | in the dependencies. So make sure to setup your spring project to include the proper version of Spring Data JPA. 29 | 30 | == How to start 31 | Three easy steps you need to do to be able to use __Spring Data JPA MongoDB Expressions__: 32 | 33 | NOTE: All public APIs (classes and interfaces) of Spring Data JPA MongoDB Expressions are in the package `com.github.mhewedy.expressions.*` 34 | 35 | 1. You need to customize the base repository to be the `ExpressionsRepositoryImpl`: 36 | + 37 | [source,java] 38 | ---- 39 | @SpringBootApplication 40 | @EnableJpaRepositories(repositoryBaseClass = ExpressionsRepositoryImpl.class) 41 | public class MyApplication { 42 | 43 | } 44 | ---- 45 | + 46 | You can learn more about customizing the base repository in the 47 | https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.customize-base-repository[spring data jpa documentations,role=external,window=_blank] 48 | 2. Change the parent repository of your JPA repositories to `ExpressionsRepository` 49 | + 50 | [source,java] 51 | ---- 52 | @Repository 53 | public interface EmployeeRepository extends ExpressionsRepository { 54 | } 55 | ---- 56 | 57 | 3. Modify the search controller to accept `Expressions` in its parameter list: 58 | + 59 | [source,java] 60 | ---- 61 | @PostMapping("/search") 62 | public ResponseEntity> search(@RequestBody Expressions expressions, 63 | Pageable pageable) { 64 | return ok().body( 65 | employeeRepository.findAll(expressions, pageable).map(employeeMapper::toDto) 66 | ); 67 | } 68 | ---- 69 | 70 | And that's it, you can now send Mongodb-like json queries to the API. (see <> ) 71 | 72 | .Literal Handling Mode in Hibernate 73 | **** 74 | As the time of writing, Hibernate by default using a 75 | https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/query/criteria/LiteralHandlingMode.html[Literal Handling mode,role=external,window=_blank] 76 | of `AUTO` when using Criteria API, which means Criteria queries uses bind parameters for any literal that is not a numeric value. 77 | You can choose to override it by setting the JPA property `hibernate.criteria.literal_handling_mode` to `bind`. 78 | 79 | .application.yaml 80 | [source,yaml] 81 | ---- 82 | spring: 83 | jpa: 84 | properties: 85 | 'hibernate.criteria.literal_handling_mode': bind 86 | ---- 87 | **** 88 | -------------------------------------------------------------------------------- /docs/images/hld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhewedy/spring-data-jpa-mongodb-expressions/d7a6c777d9874f31ebca84b785b607042c977eaf/docs/images/hld.png -------------------------------------------------------------------------------- /docs/images/sample-domain-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhewedy/spring-data-jpa-mongodb-expressions/d7a6c777d9874f31ebca84b785b607042c977eaf/docs/images/sample-domain-model.png -------------------------------------------------------------------------------- /docs/include.adoc: -------------------------------------------------------------------------------- 1 | :author: Mohammad Hewedy, The Spring Data JPA MongoDB Expressions Team 2 | :revnumber: 0.1.9 3 | :jsondir: ../src/test/resources 4 | :sectlinks: true 5 | :source-highlighter: highlight.js 6 | -------------------------------------------------------------------------------- /docs/index.adoc: -------------------------------------------------------------------------------- 1 | = Spring Data JPA MongoDB Expressions Reference Documentation 2 | :toc: left 3 | :toclevels: 3 4 | include::include.adoc[] 5 | 6 | include::introduction.adoc[leveloffset=+1] 7 | 8 | include::getting_started.adoc[leveloffset=+1] 9 | 10 | include::public_api.adoc[leveloffset=+1] 11 | 12 | include::query_specs.adoc[leveloffset=+1] 13 | 14 | include::build_expressions.adoc[leveloffset=+1] 15 | 16 | -------------------------------------------------------------------------------- /docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | = Introduction 2 | include::include.adoc[] 3 | 4 | https://github.com/mhewedy/spring-data-jpa-mongodb-expressions[__Spring Data JPA MongoDB Expressions__,role=external,window=_blank] is a library that allows you to query Spring Data JPA Repositories using 5 | https://docs.mongodb.com/manual/tutorial/query-documents/[MongoDB Query Language,role=external,window=_blank]. + 6 | It parses (a subset of) MongoDB expressions __(which itself a initiative and easy to learn Language)__ and convert them to 7 | Spring Data JPA Specifications to be used with __Spring Data JPA__ Repositories. 8 | 9 | It is specially useful for so many JavaScript clients that like to build queries using convenient MongoDB expressions and 10 | send the query to the Spring-based API to be processed by the Repository. 11 | 12 | The idea is that you can build the _MongoDB_ query-like _JSON_ from the frontend app and pass it to the Spring Controller, 13 | and then optionally enrich it with additional conditions and pass it to the JPA Repository, 14 | in which the _MongoDB_ query will be translated automatically to JPA specification and executed. 15 | 16 | .How it works 17 | **** 18 | __Spring Data JPA MongoDB Expressions__ converts the MongoDB queries into SQL queries. So the following MongoDB 19 | https://docs.mongodb.com/manual/tutorial/query-documents/#specify-and-conditions[query,role=external,window=_blank]: 20 | [source,json] 21 | ---- 22 | { "status": "A", "qty": { "$lt": 30 } } 23 | ---- 24 | 25 | will be translated to.footnote:[This is rough SQL code, as usually values are passed parameterized in the where condition.]: 26 | [source,sql] 27 | ---- 28 | SELECT * FROM inventory WHERE status = "A" AND qty < 30 29 | ---- 30 | TIP: You can create MongoDB queries using simple javascript code see <> 31 | **** 32 | -------------------------------------------------------------------------------- /docs/public_api.adoc: -------------------------------------------------------------------------------- 1 | = Public API 2 | 3 | include::include.adoc[] 4 | 5 | *Spring Data JPA MongoDB Expressions*. has 3 main public APIs (one interface and two classes) that you need to be aware of . 6 | 7 | == `ExpressionsRepository` interface 8 | 9 | [source,java] 10 | ---- 11 | public interface ExpressionsRepository extends JpaRepository { 12 | 13 | List findAll(Expressions expressions); 14 | 15 | List findAll(Expressions expressions, Sort sort); 16 | 17 | Page findAll(Expressions expressions, Pageable pageable); 18 | 19 | long count(Expressions expressions); 20 | } 21 | ---- 22 | 23 | `com.github.mhewedy.expressions.ExpressionsRepository` is the interface that your Repositories will need to extend and use 24 | its methods to pass `Expressions` objects received from the Rest Controller. 25 | 26 | As seen, you can pass `Pageable` object for sorting and paging as well. 27 | 28 | == `Expressions` class 29 | 30 | `com.github.mhewedy.expressions.Expressions` class is the class used in the controller method to deserialize the query 31 | JSON object into it. it will hold all the conditions passed from the frontend app. 32 | 33 | The user can add more conditions at the backend (e.g. to enforce more restrictions). 34 | see https://javadoc.io/doc/com.github.mhewedy/spring-data-jpa-mongodb-expressions/latest/index.html[__javadoc__,role=external,window=_blank]. 35 | 36 | Here's the public API for the `Expressions` class: 37 | [source,java] 38 | ---- 39 | public class Expressions extends HashMap { 40 | 41 | public Expressions or(Expression expression) {} 42 | 43 | public Expressions and(Expression expression) {} 44 | } 45 | ---- 46 | As shown above, it is mainly used to add ORed or ANDed expressions for an existing instance (usually 47 | deserialized from the JSON representation sent by the frontend app). 48 | 49 | NOTE: The two public methods in `Expressions` object accepts objects of type `Expression` 50 | 51 | == `Expression` class 52 | 53 | One more class is `com.github.mhewedy.expressions.Expression` which represents a single expression that can be 54 | added to the `Expressions` object. 55 | 56 | Here's the public API for the `Expression` class: 57 | [source,java] 58 | ---- 59 | public abstract class Expression { 60 | 61 | public static Expression of(String field, Operator operator, String value) {} 62 | 63 | public static Expression of(String field, Operator operator, Number value) {} 64 | 65 | // ..... 66 | // ..... 67 | 68 | public static Expression and(Expression... expressions) {} 69 | 70 | public static Expression or(Expression... expressions) {} 71 | 72 | public Expression and(Expression expression) {} 73 | 74 | public Expression or(Expression expression) {} 75 | 76 | public Expressions build() {} 77 | } 78 | ---- 79 | As shown, it is used mainly as a static factory to create `Expression` instance then convert it to `Expressions` using the 80 | `build` method or pass it to one of the two methods of the `Expressions` class that accept the `Expression` object. 81 | 82 | [.lead] 83 | Examples: 84 | 85 | * Using `Expression` static builder methods to create a single Expression and ANDing it to the `Expressions` object: 86 | 87 | [source,java] 88 | ---- 89 | Expressions expressions = ... // accepted as a parameter for the controller method 90 | 91 | expressions.and(Expression.or( 92 | Expression.of("lastName", Operator.$eq, "ibrahim"), 93 | Expression.of("age", Operator.$in, 10, 30) 94 | )); 95 | ---- 96 | 97 | * Using `Expression` static builder methods with `build` method to create `Expressions` object: 98 | 99 | [source,java] 100 | ---- 101 | Expressions expressions = Expression.of("lastName", Operator.$eq, "ibrahim") 102 | .and(Expression.or( 103 | Expression.of("age", Operator.$in, 10, 20), 104 | Expression.of("birthDate", Operator.$lt, LocalDate.of(1980, 1, 1))) 105 | ).build(); 106 | ---- 107 | -------------------------------------------------------------------------------- /docs/query_specs.adoc: -------------------------------------------------------------------------------- 1 | = Query Specifications 2 | 3 | include::include.adoc[] 4 | 5 | **** 6 | From its name, **Spring Data JPA MongoDB Expressions** inspired by 7 | https://docs.mongodb.com/manual/tutorial/query-documents/[MongoDB Query Language,role=external,window=_blank] 8 | and uses a subset of the language and converts it to Spring Data JPA Specifications under the hood. 9 | 10 | In this section we will see by example all the supported features started by basic querying to 11 | comparison operators to joins and other features. 12 | **** 13 | 14 | == Overview 15 | 16 | Before we go into details of the query language, we will go through the steps it takes to convert `JSON`-encoded 17 | `MongoDB`-like Query to SQL query. 18 | 19 | .High Level Operations 20 | image::images/hld.png[High Level Overview,align=center] 21 | 22 | The first step (yellow):: The query is created at the frontend app (Angular, React, Vue, etc...) in JSON format 23 | according to MongoDB Query language rules and as will be shown in this section. 24 | 25 | The second step (green):: The JSON is being deserialized into `com.github.mhewedy.expressions.Expressions` object 26 | and thus can be passed to your Repository (the one the extends `com.github.mhewedy.expressions.ExpressionsRepository`). 27 | + 28 | This intermediate step allows you to add additional conditions easily on the deserialized `Expressions` object as 29 | it is not uncommon you need to restrict the query passed from the frontend. 30 | 31 | NOTE: You can see the <<#_public_api,Public API>> for details on how to use the `Expressions` and `Expression` objects. 32 | 33 | The third step (red):: Where the `Expressions` object is being converted into Spring Data JPA `Specification` object 34 | and then Spring Data JPA along with the underlying Persistence provider will take care of the rest 35 | (including paging and sorting, etc...). 36 | 37 | == Queries 38 | 39 | To get an idea about the queries in this section, here's the domain that the queries will apply on: 40 | 41 | .Sample domain model 42 | image::images/sample-domain-model.png[Sample Domain Model,align=center,width=400px] 43 | 44 | NOTE: You might need to have a look on the <<#_operators,supported operators list>> before proceed to the next section. 45 | 46 | === Basic Queries 47 | 1. Basic query: 48 | + 49 | [source,json] 50 | ---- 51 | { 52 | "lastName": "ibrahim", 53 | "birthDate": {"$lte": "1985-10-10"} 54 | } 55 | ---- 56 | Generated SQL: 57 | + 58 | [source,sql] 59 | ---- 60 | ... where last_name=? and birth_date<=? 61 | ---- 62 | + 63 | .Default Comparison and Logical Operators 64 | **** 65 | In *MongoDB*, the default comparison operator if omitted is `_$eq_` (equals) and the default logical operator 66 | between two expressions is `_$and_` (AND). 67 | This is why the generated sql compares `last_name` using `$eq` operator (equals) and then use the `$and` (AND) operator between the two expressions. + 68 | You can know more about MongoDB query language from this https://docs.mongodb.com/manual/tutorial/query-documents/[link,role=external,window=_blank]. 69 | **** 70 | + 71 | 2. Basic queries with comparison operators: 72 | + 73 | [source,json] 74 | ---- 75 | include::{jsondir}/testFindAllInBaseRepository.json[] 76 | ---- 77 | Generated SQL: 78 | + 79 | [source,sql] 80 | ---- 81 | ... where last_name=? and birth_date>? and birth_date<=? 82 | ---- 83 | TIP: In case of passing a string value into a field of type `java.time` API (e.g. `LocalDate`, `Instant`, etc...), 84 | the method `parse` on the corresponding type is responsible of convert the string into an object of that type. 85 | + 86 | NOTE: However the default logical operator is `$and`, in the above example we had to add it explicitly, this is because 87 | in `JSON` you cannot have duplicated object keys (in this case would be `birthDate`). 88 | + 89 | 3. Send search query with no conditions: 90 | + 91 | [source,json] 92 | ---- 93 | {} 94 | ---- 95 | Generated SQL: 96 | + 97 | [source,sql] 98 | ---- 99 | ... where ?=1 100 | ---- 101 | NOTE: `?` in the query parameter above is bound to the value `true` 102 | + 103 | 4. Search with `null` value (`is null` and `is not null`): 104 | + 105 | [source,json] 106 | ---- 107 | include::{jsondir}/testSearchByNull.json[] 108 | ---- 109 | + 110 | [source,java] 111 | ---- 112 | // in the controller: 113 | expressions.or(Expression.of("lastName", Operator.$ne, (String) null)); 114 | ---- 115 | Generated SQL: 116 | + 117 | [source,sql] 118 | ---- 119 | ... where first_name is null or last_name is not null 120 | ---- 121 | 122 | === Logical operators 123 | 124 | According to MongoDB query language, the default logical operator if omitted is the `$and` operator. 125 | The following are examples of complex logical operators that follows operator precedence. 126 | 127 | 1. Complex case with multiple OR and AND Expressions: 128 | + 129 | [source,json] 130 | ---- 131 | include::{jsondir}/testComplexCaseWithMultipleOrAndExpressions.json[] 132 | ---- 133 | Generated SQL: 134 | + 135 | [source,sql] 136 | ---- 137 | ... where last_name = ? or first_name = ? and birth_date > ? 138 | ---- 139 | 2. Complex case with multiple OR and AND Expressions: 140 | + 141 | [source,json] 142 | ---- 143 | include::{jsondir}/testComplexCaseWithMultipleOrAndExpressions2.json[] 144 | ---- 145 | Generated SQL: 146 | + 147 | [source,sql] 148 | ---- 149 | ... where last_name = ? and (first_name = ? or birth_date < ?) 150 | ---- 151 | 152 | === Text Operators 153 | 154 | 1. Using Contains for search in fields: 155 | + 156 | [source,json] 157 | ---- 158 | include::{jsondir}/testSearchByContains.json[] 159 | ---- 160 | Generated SQL: 161 | + 162 | [source,sql] 163 | ---- 164 | ... where last_name like ? 165 | ---- 166 | 2. Using Contains ignore case for search in fields: 167 | + 168 | [source,json] 169 | ---- 170 | include::{jsondir}/testSearchByIgnoreCaseContains.json[] 171 | ---- 172 | Generated SQL: 173 | + 174 | [source,sql] 175 | ---- 176 | ... where lower(last_name) like ? 177 | ---- 178 | + 179 | 3. Using contains for number fields: 180 | + 181 | [source,json] 182 | ---- 183 | include::{jsondir}/testNumberContains.json[] 184 | ---- 185 | Generated SQL: 186 | + 187 | [source,sql] 188 | ---- 189 | ... from employee e where cast(e.age as varchar(255)) like ? 190 | ---- 191 | 192 | NOTE: Other text <<#_operators,operators>> are supported as well, such as `$start`, `$end`, `$istart`, `$iend` 193 | 194 | TIP: the `i` character at the start of the text operator donates case-insensitive nature of the operator. 195 | 196 | 197 | === Paging and sorting 198 | 199 | Due to being built on top of Spring Data JPA, __Spring Data JPA MongoDB Expressions__ library is fully compatible with the 200 | Paging and Sorting features of Spring Data JPA. 201 | 202 | 1. Paging and sorting in the url: 203 | + 204 | [source,text] 205 | ---- 206 | /api/search?page=0&size=20&sort=id,desc 207 | ---- 208 | 209 | 210 | 2. Paging and sorting in code: 211 | + 212 | [source,json] 213 | ---- 214 | include::{jsondir}/testPagingAndSorting.json[] 215 | ---- 216 | + 217 | [source,java] 218 | ---- 219 | // in the controller: 220 | Pageable pageable = PageRequest.of(0, 3, Sort.by("firstName").descending()); // or from the controller method parameters 221 | 222 | Page employeeList = employeeRepository.findAll(expressions, pageable); 223 | ---- 224 | Generated SQL: 225 | + 226 | [source,sql] 227 | ---- 228 | ... where hire_date` for right join, example on left join (department left join city): 334 | + 335 | [source,json] 336 | ---- 337 | include::{jsondir}/testLeftJoinAlternateSyntax.json[] 338 | ---- 339 | Generated SQL: 340 | + 341 | [source,sql] 342 | ---- 343 | ...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ? 344 | ---- 345 | + 346 | NOTE: `" findAll(Expressions expressions); 421 | ---- 422 | 2. Load Graph (`jakarta.persistence.loadgraph`) 423 | + 424 | 425 | This graph defines attributes that should be eagerly fetched in addition to those already marked as EAGER. 426 | Attributes not listed in the graph still follow their default fetch type. 427 | + 428 | [source,java] 429 | ---- 430 | @EntityGraph(attributePaths = {"department", "tasks"}, type = EntityGraphType.LOAD) 431 | List findAll(Expressions expressions); 432 | ---- 433 | 434 | == Operators 435 | 436 | The following is the list of supported operators: 437 | 438 | .Operators List 439 | [frame=all, grid=all,stripes=even] 440 | |=== 441 | |Operator | Description 442 | 443 | |`$eq` | col = val (if val is null then => col is null) 444 | |`$ne` | col <> val (if val is null then => col is not null) 445 | |`$ieq` | lower(col) = lower(val) 446 | |`$gt` | col > val 447 | |`$gte` | col >= val 448 | |`$lt` | col < val 449 | |`$lte` | col <= val 450 | |`$start` | col like 'val%' 451 | |`$end` | col like '%val' 452 | |`$contains`| col like '%val%' 453 | |`$istart` | lower(col) like 'lower(val)%' 454 | |`$iend` | lower(col) like '%lower(val)' 455 | |`$icontains`| lower(col) like '%lower(val)%' 456 | |`$in` | col in (val1, val2, ...) 457 | |`$nin` | col not in (val1, val2, ...) 458 | |`$or` | expr1 or expr2 459 | |`$and` | expr1 and expr2 460 | |=== 461 | 462 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhewedy/spring-data-jpa-mongodb-expressions/d7a6c777d9874f31ebca84b785b607042c977eaf/logo.png -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.0.0 10 | 11 | 12 | 13 | com.github.mhewedy 14 | spring-data-jpa-mongodb-expressions 15 | 0.1.9 16 | spring-data-jpa-mongodb-expressions 17 | Spring Data JPA Mongodb Expressions 18 | 19 | https://github.com/mhewedy/spring-data-jpa-mongodb-expressions 20 | 21 | 22 | 23 | Apache License Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0.txt 25 | 26 | 27 | 28 | 29 | 30 | Muhammad Hewedy(mhewedy) 31 | mhewedy@gmail.com 32 | mhewedy 33 | https://github.com/mhewedy 34 | 35 | 36 | 37 | 38 | scm:git:git@github.com:mhewedy/spring-data-jpa-mongodb-expressions.git 39 | scm:git:git@github.com:mhewedy/spring-data-jpa-mongodb-expressions.git 40 | 41 | git@github.com:mhewedy/spring-data-jpa-mongodb-expressions.git 42 | 43 | 44 | 45 | 17 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-data-jpa 52 | true 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | true 58 | 59 | 60 | com.fasterxml.jackson.core 61 | jackson-databind 62 | true 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-test 68 | test 69 | 70 | 71 | 72 | com.h2database 73 | h2 74 | test 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.jacoco 84 | jacoco-maven-plugin 85 | 0.8.8 86 | 87 | 88 | prepare-agent 89 | 90 | prepare-agent 91 | 92 | 93 | 94 | report 95 | test 96 | 97 | report 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.sonatype.plugins 106 | nexus-staging-maven-plugin 107 | 1.6.13 108 | true 109 | 110 | ossrh 111 | https://oss.sonatype.org/ 112 | true 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-source-plugin 118 | 2.2.1 119 | 120 | 121 | attach-sources 122 | 123 | jar-no-fork 124 | 125 | 126 | 127 | 128 | 129 | org.apache.maven.plugins 130 | maven-javadoc-plugin 131 | 2.9.1 132 | 133 | 134 | attach-javadocs 135 | 136 | jar 137 | 138 | 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-gpg-plugin 144 | 1.5 145 | 146 | 147 | sign-artifacts 148 | verify 149 | 150 | sign 151 | 152 | 153 | 154 | 155 | 156 | 157 | --pinentry-mode 158 | loopback 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | ossrh 169 | https://s01.oss.sonatype.org/content/repositories/snapshots/ 170 | 171 | 172 | ossrh 173 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import java.time.chrono.HijrahDate; 4 | import java.time.format.DateTimeFormatter; 5 | import java.time.temporal.Temporal; 6 | 7 | class DateTimeUtil { 8 | 9 | private static final DateTimeFormatter HIJRAH_ISO_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 10 | 11 | static String format(Temporal temporal) { 12 | if (temporal.getClass() == HijrahDate.class) { 13 | return ((HijrahDate) temporal).format(HIJRAH_ISO_DATE); 14 | } 15 | return temporal.toString(); 16 | } 17 | 18 | static HijrahDate parseHijrah(String text) { 19 | if (text == null) { 20 | return null; 21 | } 22 | // yyyy-MM-dd 23 | String[] date = text.trim().split("-"); 24 | if (date.length != 3) { 25 | throw new IllegalArgumentException("invalid Hijrah date: " + text); 26 | } 27 | return HijrahDate.of(Integer.parseInt(date[0]), 28 | Integer.parseInt(date[1]), 29 | Integer.parseInt(date[2])); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/Expression.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import lombok.ToString; 4 | import org.springframework.util.Assert; 5 | 6 | import java.time.temporal.Temporal; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import static java.util.stream.Collectors.toList; 12 | 13 | /** 14 | * Expression class considered the base for all other expressions as well as contains 15 | * factory method to build the expressions object. 16 | *

17 | * So it represents the java api for the expressions 18 | *

19 | *

20 | * Example: 21 | * We can build complex expressions and pass it to the spring data jpa {@link ExpressionsRepository#findAll(Expressions)} 22 | * using the factory methods in this class as following:

23 | * 24 | *

 25 |  * var expressions = Expression.of("lastName", Operator.$eq, "ibrahim")
 26 |  *         .and(Expression.or(
 27 |  *                 Expression.of("age", Operator.$in, 10, 20),
 28 |  *                 Expression.of("birthDate", Operator.$lt, LocalDate.of(1980, 1, 1)))
 29 |  *         ).build();
 30 |  * 
31 | * Then the output could be represented as: 32 | *
 33 |  * where lastName = "ibrahim" and (age in (10 , 20) or birth_date < "1980-01-01")
 34 |  * 
35 | *

36 | * The Expression API also service as an intermediate representations that resides between 37 | * the {@link Expressions} and the spring data JPA specifications 38 | * implementation {@link ExpressionsPredicateBuilder}. 39 | */ 40 | public abstract class Expression { 41 | 42 | public static Expression of(String field, Operator operator, String value) { 43 | return new SingularExpression(field, operator, value); 44 | } 45 | 46 | public static Expression of(String field, Operator operator, Number value) { 47 | return new SingularExpression(field, operator, value); 48 | } 49 | 50 | public static Expression of(String field, Operator operator, Temporal value) { 51 | return new SingularExpression(field, operator, value == null ? null : DateTimeUtil.format(value)); 52 | } 53 | 54 | public static Expression of(String field, Operator operator, Boolean value) { 55 | return new SingularExpression(field, operator, value); 56 | } 57 | 58 | public static Expression of(String field, Operator operator, Object value) { 59 | return new SingularExpression(field, operator, value); 60 | } 61 | 62 | /** 63 | * Used with operators require list of elements, 64 | * such as {@link Operator#$in} and {@link Operator#$nin} 65 | */ 66 | public static Expression of(String field, Operator operator, String... values) { 67 | return new ListExpression(field, operator, Arrays.asList(values)); 68 | } 69 | 70 | /** 71 | * Used with operators require list of elements, 72 | * such as {@link Operator#$in} and {@link Operator#$nin} 73 | */ 74 | public static Expression of(String field, Operator operator, Number... values) { 75 | return new ListExpression(field, operator, Arrays.asList(values)); 76 | } 77 | 78 | /** 79 | * Used with operators require list of elements, 80 | * such as {@link Operator#$in} and {@link Operator#$nin} 81 | */ 82 | public static Expression of(String field, Operator operator, Temporal... values) { 83 | return new ListExpression(field, operator, 84 | Arrays.stream(values).map(Object::toString).collect(toList()) 85 | ); 86 | } 87 | 88 | /** 89 | * Used with operators require list of elements, 90 | * such as {@link Operator#$in} and {@link Operator#$nin} 91 | */ 92 | public static Expression of(String field, Operator operator, Boolean... values) { 93 | return new ListExpression(field, operator, Arrays.asList(values)); 94 | } 95 | 96 | /** 97 | * Used with operators require list of elements, 98 | * such as {@link Operator#$in} and {@link Operator#$nin} 99 | */ 100 | public static Expression of(String field, Operator operator, Object... values) { 101 | return new ListExpression(field, operator, Arrays.asList(values)); 102 | } 103 | 104 | /** 105 | * Used with operators require list of elements, 106 | * such as {@link Operator#$in} and {@link Operator#$nin} 107 | */ 108 | public static Expression of(String field, Operator operator, List values) { 109 | return new ListExpression(field, operator, values); 110 | } 111 | 112 | /** 113 | * factory method used to create new expression that "and" all input expressions. 114 | */ 115 | public static Expression and(Expression... expressions) { 116 | final AndExpression andExpression = new AndExpression(); 117 | Arrays.stream(expressions).forEach(andExpression::add); 118 | return andExpression; 119 | } 120 | 121 | /** 122 | * factory method used to create new expression that "or" all input expressions. 123 | */ 124 | public static Expression or(Expression... expressions) { 125 | final OrExpression orExpression = new OrExpression(); 126 | Arrays.stream(expressions).forEach(orExpression::add); 127 | return orExpression; 128 | } 129 | 130 | /** 131 | * apply "and" on current expression with the parameter expression. 132 | */ 133 | public Expression and(Expression expression) { 134 | return new AndExpression() 135 | .add(this) 136 | .add(expression); 137 | } 138 | 139 | /** 140 | * apply "or" on current expression with the parameter expression. 141 | */ 142 | public Expression or(Expression expression) { 143 | return new OrExpression() 144 | .add(this) 145 | .add(expression); 146 | } 147 | 148 | /** 149 | * Convert current object to {@link Expressions} to be used by the {@link ExpressionsRepository}. 150 | */ 151 | public Expressions build() { 152 | return Expressions.of(this); 153 | } 154 | 155 | @ToString 156 | static class SingularExpression extends Expression { 157 | final String field; 158 | final Operator operator; 159 | final Object value; 160 | 161 | SingularExpression(String field, Operator operator, Object value) { 162 | Assert.notNull(field, "field must not be null!"); 163 | Assert.notNull(operator, "operator must not be null!"); 164 | 165 | this.field = field; 166 | this.operator = operator; 167 | this.value = value; 168 | 169 | if (operator.isList) { 170 | throw new IllegalArgumentException( 171 | String.format("operator %s accepts list of values: [%s]", operator, this) 172 | ); 173 | } 174 | } 175 | } 176 | 177 | @ToString 178 | static class ListExpression extends Expression { 179 | final String field; 180 | final Operator operator; 181 | final List values; 182 | 183 | @SuppressWarnings({"unchecked"}) 184 | ListExpression(String field, Operator operator, Object values) { 185 | if (!(values instanceof List)) { 186 | throw new IllegalArgumentException( 187 | String.format("operator %s accepts list of values: (field=%s, operator=%s, values=%s)", 188 | operator, field, operator, values) 189 | ); 190 | } 191 | List listValues = (List) values; 192 | Assert.notNull(field, "field must not be null!"); 193 | Assert.notNull(operator, "operator must not be null!"); 194 | Assert.notEmpty(listValues, "values should not be empty!"); 195 | 196 | this.field = field; 197 | this.operator = operator; 198 | this.values = listValues; 199 | } 200 | 201 | ListExpression(String field, Operator operator, List values) { 202 | Assert.notNull(field, "field must not be null!"); 203 | Assert.notNull(operator, "operator must not be null!"); 204 | Assert.notEmpty(values, "values should not be empty!"); 205 | 206 | this.field = field; 207 | this.operator = operator; 208 | this.values = values; 209 | 210 | if (!operator.isList) { 211 | throw new IllegalArgumentException( 212 | String.format("operator %s doesn't accept list of values: [%s]", operator, this) 213 | ); 214 | } 215 | } 216 | } 217 | 218 | @ToString 219 | static class OrExpression extends Expression { 220 | final List expressions = new ArrayList<>(); 221 | 222 | OrExpression add(Expression expression) { 223 | expressions.add(expression); 224 | return this; 225 | } 226 | } 227 | 228 | @ToString 229 | static class AndExpression extends Expression { 230 | final List expressions = new ArrayList<>(); 231 | 232 | AndExpression add(Expression expression) { 233 | expressions.add(expression); 234 | return this; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/Expressions.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.jpa.domain.Specification; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static com.github.mhewedy.expressions.Expression.*; 13 | import static com.github.mhewedy.expressions.Operator.$and; 14 | import static com.github.mhewedy.expressions.Operator.$or; 15 | 16 | 17 | /** 18 | * Represents Group of expression using mongodb query api. 19 | *

20 | * Example: 21 | *

 22 |  * {
 23 |  *     "status": "A",
 24 |  *     "$or": [{ "qty": { "$lt": 30 } }, { "item": { "$in": ["A", "D"] } }]
 25 |  * }
 26 |  * 
27 | *

28 | * Support a list of Operators defined in {@link Operator}. 29 | * 30 | * @see ExpressionsRepository#findAll(Expressions) 31 | * @see ExpressionsRepository#findAll(Expressions, Sort) 32 | * @see ExpressionsRepository#findAll(Expressions, Pageable) 33 | * @see Mongo Query Documents 34 | */ 35 | public class Expressions extends HashMap { 36 | 37 | /** 38 | * Create a new $or expression on the root, 39 | * then adds to it the current expressions attached at the root 40 | * and the expression passes as parameter. 41 | *

42 | * Example: 43 | * Suppose we have the following expression as json:

 {"firstName": "ali"} 
44 | *

45 | * Then we added the following: 46 | *

 47 |      *  expressions.or(Expression.and(
 48 |      *          Expression.of("lastName", $eq, "ibrahim"),
 49 |      *          Expression.of("age", $gte, 10)
 50 |      *  ));
 51 |      * 
52 | * Then the output could be represented as: 53 | * 54 | *
 55 |      * {
 56 |      *     "$or": [
 57 |      *          {"firstName": "ali"},
 58 |      *          {
 59 |      *              "$and": [
 60 |      *                  {"lastName": "ibrahim"},
 61 |      *                  {"age": {"$gte": 10}},
 62 |      *              ]
 63 |      *          }
 64 |      *     ]
 65 |      * }
 66 |      * 
67 | * Or in sql as: 68 | *
 69 |      *  where firstName = "ali" or lastName = "ibrahim" and age >= 10
 70 |      * 
71 | */ 72 | public Expressions or(Expression expression) { 73 | return append(expression, $or); 74 | } 75 | 76 | /** 77 | * Add the parameter expression to the list of expression in the expressions object. 78 | *

79 | * Example: 80 | * Suppose we have the following expression as json: 81 | *

 {"firstName": "ali"} 
82 | *

83 | * Then we added the following: 84 | *

 85 |      *  expressions.and(Expression.of("birthDate", $gt, "1980-10-10"));
 86 |      *  expressions.and(Expression.or(
 87 |      *      Expression.of("lastName", $eq, "ibrahim"),
 88 |      *      Expression.of("age", $in, 10, 30)
 89 |      *  ));
 90 |      *
 91 |      * 
92 | * Then the output could be represented as: 93 | * 94 | *
 95 |      * {
 96 |      *     "firstName": "ali",
 97 |      *     "birthDate": {"$gt": "1980-10-10"},
 98 |      *     "$or": [
 99 |      *          {"lastName": "ibrahim"},
100 |      *          {"age": {"$in": [10, 30]}}
101 |      *     ]
102 |      * }
103 |      * 
104 | * Or in sql as: 105 | *
106 |      * where firstName = "ali" and birthDate > "1980-10-10"
107 |      *     and (lastName = "ibrahim" or age in (10, 30) )
108 |      * 
109 | */ 110 | public Expressions and(Expression expression) { 111 | return append(expression, $and); 112 | } 113 | 114 | private Expressions append(Expression expression, Operator operator) { 115 | Map tmp = new HashMap<>(this); 116 | this.clear(); 117 | 118 | Map map = new HashMap<>(); 119 | addToMap(expression, map); 120 | 121 | List> list = new ArrayList<>(); 122 | list.add(map); 123 | if (!tmp.isEmpty()) list.add(tmp); 124 | 125 | this.put(operator.name(), list); 126 | 127 | return this; 128 | } 129 | 130 | public Specification getSpecification() { 131 | return new ExpressionsRepositoryImpl.ExpressionsSpecification<>(this); 132 | } 133 | 134 | static Expressions of(Expression expression) { 135 | Expressions expressions = new Expressions(); 136 | expressions.and(expression); 137 | return expressions; 138 | } 139 | 140 | /** 141 | * The bridge between {@link Expression} and the internal representation of mongodb query lang 142 | * represented by {@link Expressions} class. 143 | */ 144 | private static void addToMap(Expression expression, Map map) { 145 | if (expression instanceof SingularExpression se) { 146 | if (se.operator == Operator.$eq) { 147 | map.put(se.field, se.value); 148 | } else { 149 | map.put(se.field, mapOf(se.operator.name(), se.value)); 150 | } 151 | } else if (expression instanceof ListExpression le) { 152 | if (le.operator == Operator.$eq) { 153 | map.put(le.field, le.values); 154 | } else { 155 | map.put(le.field, mapOf(le.operator.name(), le.values)); 156 | } 157 | } else if (expression instanceof OrExpression oe) { 158 | map.put($or.name(), 159 | oe.expressions.stream().map(it -> { 160 | Map m = new HashMap<>(); 161 | addToMap(it, m); 162 | return m; 163 | }).toList() 164 | ); 165 | } else if (expression instanceof AndExpression ae) { 166 | map.put($and.name(), 167 | ae.expressions.stream().map(it -> { 168 | Map m = new HashMap<>(); 169 | addToMap(it, m); 170 | return m; 171 | }).toList() 172 | ); 173 | } 174 | } 175 | 176 | List getExpressions() { 177 | return getExpressions(this); 178 | } 179 | 180 | /** 181 | * Returns this object as list of {@link Expression} to be passed to 182 | * Spring Data Specification builder {@link ExpressionsPredicateBuilder} 183 | */ 184 | @SuppressWarnings({"unchecked"}) 185 | private static List getExpressions(Map map) { 186 | 187 | List expressions = new ArrayList<>(); 188 | 189 | for (Entry entry : map.entrySet()) { 190 | 191 | String key = entry.getKey(); 192 | Object value = entry.getValue(); 193 | 194 | if ($or.name().equalsIgnoreCase(key)) { 195 | List> valueList = (List>) value; 196 | 197 | OrExpression orExpression = new OrExpression(); 198 | expressions.add(orExpression); 199 | 200 | for (Map valueMap : valueList) { 201 | orExpression.expressions.add(getExpressions(valueMap).get(0)); 202 | } 203 | } else if ($and.name().equalsIgnoreCase(key)) { 204 | List> valueList = (List>) value; 205 | 206 | AndExpression andExpression = new AndExpression(); 207 | expressions.add(andExpression); 208 | 209 | for (Map valueMap : valueList) { 210 | andExpression.expressions.add(getExpressions(valueMap).get(0)); 211 | } 212 | } else { 213 | if (value instanceof Map) { // value in the form of {"$operator": "value"} 214 | Map valueMap = ((Map) value); 215 | Entry first = valueMap.entrySet().iterator().next(); 216 | 217 | Operator operator = Operator.valueOf(first.getKey()); 218 | 219 | if (operator.isList) { 220 | expressions.add(new ListExpression(key, operator, first.getValue())); 221 | } else { 222 | expressions.add(new SingularExpression(key, operator, first.getValue())); 223 | } 224 | } else { // operator is "$eq" 225 | expressions.add(new SingularExpression(key, Operator.$eq, value)); 226 | } 227 | } 228 | } 229 | 230 | return expressions; 231 | } 232 | 233 | private static Map mapOf(String key, Object value) { 234 | Map m = new HashMap<>(); 235 | m.put(key, value); 236 | return m; 237 | } 238 | 239 | /** 240 | * Extracts all field names and their corresponding values from the current 241 | * {@code Expressions} object, including nested fields within `$and` and `$or` 242 | * compound operators. 243 | *

244 | * This method traverses the structure of the {@code Expressions} object recursively. 245 | * If a compound operator (`$and` or `$or`) is encountered, it extracts fields and 246 | * values from all nested expressions. 247 | *

248 | *

249 | * Example: 250 | * Given the following {@code Expressions} structure: 251 | *

252 |      * {
253 |      *     "firstName": "John",
254 |      *     "$or": [
255 |      *         {"lastName": "Doe"},
256 |      *         {"age": {"$gt": 30}}
257 |      *     ]
258 |      * }
259 |      * 
260 | * The resulting map of fields will be: 261 | *
262 |      * {
263 |      *     "firstName": "John",
264 |      *     "lastName": "Doe",
265 |      *     "age": 30
266 |      * }
267 |      * 
268 | * 269 | * @return a map containing field names as keys and their corresponding values, including nested fields. 270 | */ 271 | public Map extractFields() { 272 | return extractFields(getExpressions(this)); 273 | } 274 | 275 | private static Map extractFields(List expressionList) { 276 | var map = new HashMap(); 277 | for (Expression expression : expressionList) { 278 | if (expression instanceof SingularExpression singularExpression) { 279 | map.put(singularExpression.field, singularExpression.value); 280 | } else if (expression instanceof ListExpression listExpression) { 281 | map.put(listExpression.field, listExpression.values); 282 | } else if (expression instanceof AndExpression andExpression) { 283 | map.putAll(extractFields(andExpression.expressions)); 284 | } else if (expression instanceof OrExpression andExpression) { 285 | map.putAll(extractFields(andExpression.expressions)); 286 | } 287 | } 288 | return map; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/ExpressionsPredicateBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import jakarta.persistence.criteria.*; 4 | import jakarta.persistence.metamodel.Attribute; 5 | import jakarta.persistence.metamodel.ManagedType; 6 | import jakarta.persistence.metamodel.PluralAttribute; 7 | import jakarta.persistence.metamodel.SingularAttribute; 8 | import org.springframework.util.Assert; 9 | 10 | import java.time.*; 11 | import java.time.chrono.HijrahDate; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.UUID; 16 | import java.util.stream.Collectors; 17 | 18 | import static com.github.mhewedy.expressions.Expression.*; 19 | import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType; 20 | import static java.util.Collections.singletonList; 21 | import static java.util.stream.Collectors.toList; 22 | 23 | class ExpressionsPredicateBuilder { 24 | 25 | static Predicate getPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb, Expressions expressions) { 26 | Assert.notNull(expressions, "expressions must not be null!"); 27 | 28 | List predicates = getPredicates(query, cb, 29 | root, 30 | root.getModel(), 31 | expressions.getExpressions()); 32 | 33 | if (predicates.isEmpty()) { 34 | return cb.isTrue(cb.literal(true)); 35 | } 36 | 37 | if (predicates.size() == 1) { 38 | return predicates.iterator().next(); 39 | } 40 | 41 | Predicate[] array = predicates.toArray(new Predicate[0]); 42 | 43 | return cb.and(array); 44 | } 45 | 46 | @SuppressWarnings({"rawtypes", "unchecked"}) 47 | private static List getPredicates(CriteriaQuery query, CriteriaBuilder cb, 48 | Path from, ManagedType type, 49 | List expressions) { 50 | 51 | List predicates = new ArrayList<>(); 52 | 53 | for (Expression expression : expressions) { 54 | 55 | if (expression instanceof SingularExpression singularExpression) { 56 | 57 | final String field = extractField(singularExpression.field); 58 | Attribute attribute = getAttribute(type, field); 59 | 60 | if (attribute.isAssociation()) { 61 | if (attribute instanceof PluralAttribute) { 62 | query.distinct(true); 63 | } 64 | 65 | final SubField subField = extractSubField(singularExpression.field); 66 | if (!subField.name.isEmpty()) { 67 | final SingularExpression subExpression = 68 | new SingularExpression(subField.name, singularExpression.operator, singularExpression.value); 69 | predicates.addAll( 70 | getPredicates(query, cb, 71 | reuseOrCreateJoin((From) from, attribute, field, subField.joinType), 72 | extractSubFieldType(attribute), 73 | singletonList(subExpression) 74 | ) 75 | ); 76 | continue; 77 | } 78 | } 79 | 80 | Path exprPath = from.get((SingularAttribute) attribute); 81 | 82 | if (PersistentAttributeType.EMBEDDED == attribute.getPersistentAttributeType()) { 83 | final SubField subField = extractSubField(singularExpression.field); 84 | attribute = extractSubFieldType(attribute).getAttribute(subField.name); 85 | exprPath = exprPath.get((SingularAttribute) attribute); 86 | } 87 | 88 | Object attributeValue = convertValueToAttributeType(singularExpression.value, attribute.getJavaType()); 89 | Predicate predicate; 90 | 91 | switch (singularExpression.operator) { 92 | // equality 93 | case $eq: 94 | if (attributeValue == null) { 95 | predicate = cb.isNull(exprPath); 96 | } else { 97 | predicate = cb.equal(exprPath, attributeValue); 98 | } 99 | break; 100 | case $ieq: 101 | predicate = cb.equal(cb.lower(exprPath), ((String) attributeValue).toLowerCase()); 102 | break; 103 | case $ne: 104 | if (attributeValue == null) { 105 | predicate = cb.isNotNull(exprPath); 106 | } else { 107 | predicate = cb.notEqual(exprPath, attributeValue); 108 | } 109 | break; 110 | 111 | // comparison 112 | case $gt: 113 | if (Number.class.isAssignableFrom(attribute.getJavaType())) { 114 | predicate = cb.gt(exprPath, (Number) attributeValue); 115 | } else if (Comparable.class.isAssignableFrom(attribute.getJavaType())) { 116 | predicate = cb.greaterThan(exprPath, (Comparable) attributeValue); 117 | } else { 118 | throw new IllegalArgumentException("field should be Number or Comparable: " + singularExpression); 119 | } 120 | break; 121 | case $gte: 122 | if (Number.class.isAssignableFrom(attribute.getJavaType())) { 123 | predicate = cb.ge(exprPath, (Number) attributeValue); 124 | } else if (Comparable.class.isAssignableFrom(attribute.getJavaType())) { 125 | predicate = cb.greaterThanOrEqualTo(exprPath, (Comparable) attributeValue); 126 | } else { 127 | throw new IllegalArgumentException("field should be Number or Comparable: " + singularExpression); 128 | } 129 | break; 130 | case $lt: 131 | if (Number.class.isAssignableFrom(attribute.getJavaType())) { 132 | predicate = cb.lt(exprPath, (Number) attributeValue); 133 | } else if (Comparable.class.isAssignableFrom(attribute.getJavaType())) { 134 | predicate = cb.lessThan(exprPath, (Comparable) attributeValue); 135 | } else { 136 | throw new IllegalArgumentException("field should be Number or Comparable: " + singularExpression); 137 | } 138 | break; 139 | case $lte: 140 | if (Number.class.isAssignableFrom(attribute.getJavaType())) { 141 | predicate = cb.le(exprPath, (Number) attributeValue); 142 | } else if (Comparable.class.isAssignableFrom(attribute.getJavaType())) { 143 | predicate = cb.lessThanOrEqualTo(exprPath, (Comparable) attributeValue); 144 | } else { 145 | throw new IllegalArgumentException("field should be Number or Comparable: " + singularExpression); 146 | } 147 | break; 148 | // like 149 | case $start: 150 | predicate = cb.like(exprPath.as(String.class), attributeValue + "%"); 151 | break; 152 | case $end: 153 | predicate = cb.like(exprPath.as(String.class), "%" + attributeValue); 154 | break; 155 | case $contains: 156 | predicate = cb.like(exprPath.as(String.class), "%" + attributeValue + "%"); 157 | break; 158 | case $istart: 159 | predicate = cb.like(cb.lower(exprPath.as(String.class)), attributeValue.toString().toLowerCase() + "%"); 160 | break; 161 | case $iend: 162 | predicate = cb.like(cb.lower(exprPath.as(String.class)), "%" + attributeValue.toString().toLowerCase()); 163 | break; 164 | case $icontains: 165 | predicate = cb.like(cb.lower(exprPath.as(String.class)), "%" + attributeValue.toString().toLowerCase() + "%"); 166 | break; 167 | default: 168 | throw new IllegalStateException("Unexpected value: " + singularExpression); 169 | } 170 | 171 | predicates.add(predicate); 172 | 173 | } else if (expression instanceof ListExpression listExpression) { 174 | 175 | final String field = extractField(listExpression.field); 176 | Attribute attribute = getAttribute(type, field); 177 | 178 | if (attribute.isAssociation()) { 179 | if (attribute instanceof PluralAttribute) { 180 | query.distinct(true); 181 | } 182 | 183 | final SubField subField = extractSubField(listExpression.field); 184 | if (!subField.name.isEmpty()) { 185 | final ListExpression subExpression = 186 | new ListExpression(subField.name, listExpression.operator, listExpression.values); 187 | predicates.addAll( 188 | getPredicates(query, cb, 189 | reuseOrCreateJoin((From) from, attribute, field, subField.joinType), 190 | extractSubFieldType(attribute), 191 | singletonList(subExpression) 192 | ) 193 | ); 194 | continue; 195 | } 196 | } 197 | 198 | Path exprPath = from.get((SingularAttribute) attribute); 199 | 200 | if (PersistentAttributeType.EMBEDDED == attribute.getPersistentAttributeType()) { 201 | final SubField subField = extractSubField(listExpression.field); 202 | attribute = extractSubFieldType(attribute).getAttribute(subField.name); 203 | exprPath = exprPath.get((SingularAttribute) attribute); 204 | } 205 | 206 | List attributeValues = convertValueToAttributeType(listExpression.values, attribute.getJavaType()); 207 | 208 | Predicate predicate; 209 | switch (listExpression.operator) { 210 | // in 211 | case $in: 212 | CriteriaBuilder.In in = cb.in(exprPath); 213 | attributeValues.forEach(in::value); 214 | predicate = in; 215 | break; 216 | case $nin: 217 | CriteriaBuilder.In inx = cb.in(exprPath); 218 | attributeValues.forEach(inx::value); 219 | predicate = cb.not(inx); 220 | break; 221 | default: 222 | throw new IllegalStateException("Unexpected value: " + listExpression); 223 | } 224 | predicates.add(predicate); 225 | 226 | } else if (expression instanceof OrExpression) { 227 | predicates.add(cb.or( 228 | getPredicates(query, cb, from, type, 229 | ((OrExpression) expression).expressions).toArray(new Predicate[0]) 230 | )); 231 | 232 | } else if (expression instanceof AndExpression) { 233 | predicates.add(cb.and( 234 | getPredicates(query, cb, from, type, 235 | ((AndExpression) expression).expressions).toArray(new Predicate[0]) 236 | )); 237 | } 238 | } 239 | 240 | return predicates; 241 | } 242 | 243 | private static Attribute getAttribute(ManagedType type, String field) { 244 | try { 245 | return type.getAttribute(field); 246 | } catch (IllegalArgumentException ex) { 247 | throw new IllegalArgumentException( 248 | String.format( 249 | "Unable to locate attribute with the given name [%s] on this ManagedType [%s]," + 250 | " Are you sure this ManagedType or one of its ancestors contains such attribute?", 251 | field, 252 | type.getJavaType().getName() 253 | ) 254 | ); 255 | } 256 | } 257 | 258 | private static Path reuseOrCreateJoin(From from, Attribute attribute, String field, JoinType joinType) { 259 | return from.getJoins().stream() 260 | .filter(it -> it.getAttribute() == attribute) 261 | .findFirst() 262 | .orElseGet(() -> from.join(field, joinType)); 263 | } 264 | 265 | @SuppressWarnings({"rawtypes"}) 266 | private static ManagedType extractSubFieldType(Attribute attribute) { 267 | return (ManagedType) (attribute.isCollection() ? ((PluralAttribute) attribute).getElementType() : 268 | (((SingularAttribute) attribute).getType())); 269 | } 270 | 271 | private static String extractField(String field) { 272 | return field.contains(".") ? field.split("\\.")[0] 273 | .replaceAll("^[<>]+", "") // remove '<' and '>' at start (left/right join indicators) 274 | .replaceAll("\\?$", "") // remove '?' at end (left join indicators - optional chaining operator) 275 | : field; 276 | } 277 | 278 | private static SubField extractSubField(String field) { 279 | //if field is "abc.efg.xyz", then mainField=>"abc" and subField => "efg.xyz", so to support n-level association 280 | String mainField = Arrays.stream(field.split("\\.")).limit(1).collect(Collectors.joining(".")); 281 | String subField = Arrays.stream(field.split("\\.")).skip(1).collect(Collectors.joining(".")); 282 | 283 | JoinType joinType = mainField.startsWith("<") || mainField.endsWith("?") ? JoinType.LEFT // ") ? JoinType.RIGHT // >abc 285 | : JoinType.INNER; // abc 286 | return new SubField(subField, joinType); 287 | } 288 | 289 | private record SubField(String name, JoinType joinType) { 290 | } 291 | 292 | @SuppressWarnings({"rawtypes", "unchecked"}) 293 | private static Object convertValueToAttributeType(Object value, Class javaType) { 294 | if (value == null) { 295 | return null; 296 | } 297 | if (javaType.equals(LocalDate.class)) { 298 | return LocalDate.parse((CharSequence) value); 299 | } 300 | if (javaType.equals(Instant.class)) { 301 | return Instant.parse((CharSequence) value); 302 | } 303 | if (javaType.equals(LocalDateTime.class)) { 304 | return LocalDateTime.parse((CharSequence) value); 305 | } 306 | if (javaType.equals(OffsetDateTime.class)) { 307 | return OffsetDateTime.parse((CharSequence) value); 308 | } 309 | if (javaType.equals(ZonedDateTime.class)) { 310 | return ZonedDateTime.parse((CharSequence) value); 311 | } 312 | if (javaType.equals(HijrahDate.class)) { 313 | return DateTimeUtil.parseHijrah((String) value); 314 | } 315 | if (javaType.isEnum()) { 316 | if (Number.class.isAssignableFrom(value.getClass())) { 317 | return javaType.getEnumConstants()[((Number) value).intValue()]; 318 | } else if (String.class.isAssignableFrom(value.getClass())) { 319 | return Enum.valueOf(javaType, (String) value); 320 | } else { 321 | return Enum.valueOf(javaType, ((Enum) value).name()); 322 | } 323 | } 324 | if (javaType.equals(UUID.class)) { 325 | return UUID.fromString((String) value); 326 | } 327 | 328 | // strings and numeric types don't need conversion 329 | return value; 330 | } 331 | 332 | @SuppressWarnings({"rawtypes"}) 333 | private static List convertValueToAttributeType(List values, Class javaType) { 334 | if (values == null || values.isEmpty() || values.get(0).getClass().equals(javaType)) { 335 | return values; 336 | } 337 | 338 | if (javaType.equals(Short.class)) { 339 | return values.stream().map(it -> ((Integer) it).shortValue()).collect(toList()); 340 | } 341 | if (javaType.equals(Long.class)) { 342 | return values.stream().map(it -> ((Integer) it).longValue()).collect(toList()); 343 | } 344 | if (javaType.equals(Byte.class)) { 345 | return values.stream().map(it -> ((Integer) it).byteValue()).collect(toList()); 346 | } 347 | if (javaType.equals(Double.class)) { 348 | return values.stream().map(it -> ((Integer) it).doubleValue()).collect(toList()); 349 | } 350 | if (javaType.equals(Float.class)) { 351 | return values.stream().map(it -> ((Integer) it).floatValue()).collect(toList()); 352 | } 353 | 354 | return values.stream().map(it -> convertValueToAttributeType(it, javaType)).collect(toList()); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/ExpressionsRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.repository.NoRepositoryBean; 8 | 9 | import java.util.List; 10 | 11 | @NoRepositoryBean 12 | public interface ExpressionsRepository extends JpaRepository { 13 | 14 | List findAll(Expressions expressions); 15 | 16 | List findAll(Expressions expressions, Sort sort); 17 | 18 | Page findAll(Expressions expressions, Pageable pageable); 19 | 20 | long count(Expressions expressions); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/ExpressionsRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.SneakyThrows; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.domain.Sort; 10 | import org.springframework.data.jpa.domain.Specification; 11 | import org.springframework.data.jpa.repository.support.JpaEntityInformation; 12 | import org.springframework.data.jpa.repository.support.SimpleJpaRepository; 13 | import org.springframework.util.ClassUtils; 14 | 15 | import jakarta.persistence.EntityManager; 16 | import jakarta.persistence.criteria.CriteriaBuilder; 17 | import jakarta.persistence.criteria.CriteriaQuery; 18 | import jakarta.persistence.criteria.Predicate; 19 | import jakarta.persistence.criteria.Root; 20 | import java.util.List; 21 | 22 | @Slf4j 23 | public class ExpressionsRepositoryImpl 24 | extends SimpleJpaRepository implements ExpressionsRepository { 25 | 26 | private static Object OBJECT_MAPPER; 27 | private static final boolean OBJECT_MAPPER_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", 28 | ExpressionsRepositoryImpl.class.getClassLoader()); 29 | 30 | static { 31 | if (OBJECT_MAPPER_PRESENT) { 32 | OBJECT_MAPPER = new ObjectMapper(); 33 | } 34 | } 35 | 36 | public ExpressionsRepositoryImpl(JpaEntityInformation 37 | entityInformation, EntityManager entityManager) { 38 | super(entityInformation, entityManager); 39 | } 40 | 41 | @Override 42 | public List findAll(Expressions expressions) { 43 | return findAll(new ExpressionsSpecification<>(expressions)); 44 | } 45 | 46 | @Override 47 | public List findAll(Expressions expressions, Sort sort) { 48 | return findAll(new ExpressionsSpecification<>(expressions), sort); 49 | } 50 | 51 | @Override 52 | public Page findAll(Expressions expressions, Pageable pageable) { 53 | return findAll(new ExpressionsSpecification<>(expressions), pageable); 54 | } 55 | 56 | @Override 57 | public long count(Expressions expressions) { 58 | return count(new ExpressionsSpecification<>(expressions)); 59 | } 60 | 61 | @RequiredArgsConstructor 62 | static class ExpressionsSpecification implements Specification { 63 | 64 | private final Expressions expressions; 65 | 66 | @Override 67 | public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { 68 | logExpressions(); 69 | return ExpressionsPredicateBuilder.getPredicate(root, query, cb, expressions); 70 | } 71 | 72 | @SneakyThrows 73 | private void logExpressions() { 74 | if (!log.isDebugEnabled()) { 75 | return; 76 | } 77 | log.debug("expressions: {}", OBJECT_MAPPER_PRESENT ? ((ObjectMapper) OBJECT_MAPPER).writeValueAsString(expressions) : expressions); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/github/mhewedy/expressions/Operator.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | /** 4 | * List of operators supported and used in the {@link Expressions} 5 | */ 6 | public enum Operator { 7 | 8 | $eq(false), // col = val (if val is null then => col is null) 9 | $ne(false), // col <> val (if val is null then => col is not null) 10 | $ieq(false), // lower(col) = lower(val) 11 | 12 | $gt(false), // col > val 13 | $gte(false), // col >= val 14 | $lt(false), // col < val 15 | $lte(false), // col <= val 16 | 17 | $start(false), // col like 'val%' 18 | $end(false), // col like '%val' 19 | $contains(false), // col like '%val%' 20 | $istart(false), // lower(col) like 'lower(val)%' 21 | $iend(false), // lower(col) like '%lower(val)' 22 | $icontains(false), // lower(col) like '%lower(val)%' 23 | 24 | $in(true), // col in (val1, val2, ...) 25 | $nin(true), // col not in (val1, val2, ...) 26 | 27 | $or(false), // expr1 or expr2 28 | $and(false); // expr1 and expr2 29 | 30 | public final boolean isList; 31 | 32 | Operator(boolean isList) { 33 | this.isList = isList; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/BookRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import com.github.mhewedy.expressions.model.Book; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface BookRepository extends ExpressionsRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import com.github.mhewedy.expressions.model.Employee; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface EmployeeRepository extends ExpressionsRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/ExpressionsRepositoryImplTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.mhewedy.expressions.model.*; 6 | import jakarta.persistence.EntityManager; 7 | import jakarta.persistence.criteria.CriteriaBuilder; 8 | import jakarta.persistence.criteria.CriteriaQuery; 9 | import jakarta.persistence.criteria.Root; 10 | import lombok.SneakyThrows; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; 17 | import org.springframework.boot.autoconfigure.domain.EntityScan; 18 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 19 | import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; 20 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 21 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 22 | import org.springframework.data.domain.Page; 23 | import org.springframework.data.domain.PageRequest; 24 | import org.springframework.data.domain.Sort; 25 | import org.springframework.data.jpa.domain.Specification; 26 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 27 | import org.springframework.test.context.ContextConfiguration; 28 | import org.springframework.util.ResourceUtils; 29 | 30 | import java.io.File; 31 | import java.nio.file.Files; 32 | import java.time.Instant; 33 | import java.time.LocalDate; 34 | import java.time.chrono.HijrahDate; 35 | import java.util.Arrays; 36 | import java.util.List; 37 | import java.util.UUID; 38 | 39 | import static com.github.mhewedy.expressions.Operator.*; 40 | import static com.github.mhewedy.expressions.model.Employee.Lang; 41 | import static com.github.mhewedy.expressions.model.Status.ACTIVE; 42 | import static com.github.mhewedy.expressions.model.Status.NOT_ACTIVE; 43 | import static org.assertj.core.api.Assertions.assertThat; 44 | import static org.assertj.core.api.Assertions.assertThatList; 45 | import static org.assertj.core.api.AssertionsForClassTypes.fail; 46 | 47 | @Slf4j 48 | @DataJpaTest 49 | @ContextConfiguration(classes = {JpaRepositoriesAutoConfiguration.class, DataSourceAutoConfiguration.class}) 50 | @EntityScan("com.github.mhewedy.expressions") 51 | @EnableJpaRepositories(repositoryBaseClass = ExpressionsRepositoryImpl.class, basePackages = "com.github.mhewedy.expressions") 52 | @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) 53 | public class ExpressionsRepositoryImplTest { 54 | 55 | private List allEmployees; 56 | 57 | @Autowired 58 | EntityManager entityManager; 59 | @Autowired 60 | private BookRepository bookRepository; 61 | @Autowired 62 | private EmployeeRepository employeeRepository; 63 | 64 | @BeforeEach 65 | public void setup() { 66 | log.info("setting up"); 67 | allEmployees = Arrays.asList( 68 | new Employee(null, 69 | "ahmed", 70 | "ibrahim", 71 | new LingualString("ahmed ar", "ahmed en"), 72 | LocalDate.of(1980, 10, 10), 73 | HijrahDate.of(1380, 10, 10), 74 | 10, 75 | Instant.parse("2007-12-03T10:15:30.00Z"), 76 | (short) 1, 77 | true, 78 | new Department(null, "hr", new City(null, "cairo")), 79 | Arrays.asList(new Task(null, "fix hr", ACTIVE), new Task(null, "fix hr", ACTIVE)), 80 | UUID.fromString("2dfb7bc7-38a6-4826-b6d3-297969d17244"), 81 | Lang.AR, 82 | Lang.AR 83 | ), 84 | new Employee(null, 85 | "mohammad", 86 | "ibrahim", 87 | new LingualString("mohammad ar", "mohammad en"), 88 | LocalDate.of(1985, 10, 10), 89 | HijrahDate.of(1385, 10, 10), 90 | 20, 91 | Instant.parse("2009-12-03T10:15:30.00Z"), 92 | (short) 1, 93 | true, 94 | new Department(null, "sw arch", null), 95 | Arrays.asList(new Task(null, "fix sw arch", ACTIVE), new Task(null, "fix sw arch", ACTIVE)), 96 | UUID.randomUUID(), 97 | Lang.AR, 98 | Lang.AR 99 | ), 100 | new Employee(null, 101 | "mostafa", 102 | "ahmed", 103 | new LingualString("mostafa ar", "mostafa en"), 104 | LocalDate.of(1988, 10, 10), 105 | HijrahDate.of(1388, 10, 10), 106 | 30, 107 | Instant.parse("2011-12-03T10:15:30.00Z"), 108 | (short) 2, 109 | true, 110 | new Department(null, "sw dev", new City(null, "alex")), 111 | Arrays.asList(new Task(null, "fix sw dev", ACTIVE), new Task(null, "fix sw dev", ACTIVE)), 112 | UUID.randomUUID(), 113 | Lang.AR, 114 | Lang.AR 115 | ), 116 | new Employee(null, 117 | "wael", 118 | "ibrahim", 119 | new LingualString("wael ar", "wael en"), 120 | LocalDate.of(1990, 10, 10), 121 | HijrahDate.of(1390, 10, 10), 122 | 40, 123 | Instant.parse("2015-12-03T10:15:30.00Z"), 124 | (short) 2, 125 | true, 126 | new Department(null, "hr", new City(null, "cairo")), 127 | Arrays.asList(new Task(null, "fix hr", ACTIVE), new Task(null, "fix hr", ACTIVE)), 128 | UUID.randomUUID(), 129 | Lang.AR, 130 | Lang.AR 131 | ), 132 | new Employee(null, 133 | "farida", 134 | "abdullah", 135 | new LingualString("farida ar", "farida en"), 136 | LocalDate.of(1979, 10, 10), 137 | HijrahDate.of(1379, 10, 10), 138 | 50, 139 | Instant.parse("2017-12-03T10:15:30.00Z"), 140 | (short) 2, 141 | false, 142 | new Department(null, "hr", new City(null, "cairo")), 143 | Arrays.asList(new Task(null, "fix hr", ACTIVE), new Task(null, "fix hr", NOT_ACTIVE)), 144 | UUID.randomUUID(), 145 | Lang.AR, 146 | Lang.AR 147 | ), 148 | new Employee(null, 149 | "fofo", 150 | "bobo", 151 | new LingualString("fofo ar", "fofo en"), 152 | LocalDate.of(1979, 10, 10), 153 | HijrahDate.of(1379, 10, 10), 154 | 50, 155 | Instant.parse("2017-12-03T10:15:30.00Z"), 156 | (short) 2, 157 | false, 158 | null, 159 | null, 160 | UUID.randomUUID(), 161 | Lang.EN, 162 | Lang.EN 163 | ) 164 | ); 165 | employeeRepository.saveAll(allEmployees); 166 | 167 | bookRepository.saveAll(List.of( 168 | new Book(new Book.BookId("Spring in Action", "English"), "Craig Walls"), 169 | new Book(new Book.BookId("Kubernetes Up and Running", "English"), "Brendan Burns") 170 | )); 171 | } 172 | 173 | @AfterEach 174 | public void finish() { 175 | log.info("cleaning"); 176 | employeeRepository.deleteAll(); 177 | } 178 | 179 | @Test 180 | public void testFindAllInBaseRepository() throws Exception { 181 | 182 | String json = loadResourceJsonFile("testFindAllInBaseRepository"); 183 | 184 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 185 | 186 | List employeeList = employeeRepository.findAll(expressions); 187 | assertThat(employeeList).isNotNull(); 188 | assertThat(employeeList.size()).isEqualTo(1); 189 | assertThat(employeeList.get(0).firstName).isEqualTo("mohammad"); 190 | 191 | // where last_name=? and birth_date>? and birth_date<=? 192 | } 193 | 194 | @Test 195 | public void testPagingAndSorting() throws Exception { 196 | 197 | String json = loadResourceJsonFile("testPagingAndSorting"); 198 | 199 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 200 | 201 | Page employeeList = 202 | employeeRepository.findAll(expressions, PageRequest.of(0, 3, Sort.by("firstName").descending())); 203 | assertThat(employeeList).isNotNull(); 204 | assertThat(employeeList.getTotalElements()).isEqualTo(6); 205 | assertThat(employeeList.getSize()).isEqualTo(3); 206 | assertThat(employeeList.getContent().get(0).firstName).isEqualTo("wael"); 207 | } 208 | 209 | @Test 210 | public void testComplexCaseWithMultipleOrAndExpressions() throws Exception { 211 | 212 | String json = loadResourceJsonFile("testComplexCaseWithMultipleOrAndExpressions"); 213 | 214 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 215 | 216 | List employeeList = employeeRepository.findAll(expressions); 217 | assertThat(employeeList).isNotNull(); 218 | assertThat(employeeList.size()).isEqualTo(3); 219 | // where last_name = ? or first_name = ? and birth_date > ? 220 | } 221 | 222 | @Test 223 | public void testComplexCaseWithMultipleOrAndExpressions2() throws Exception { 224 | 225 | String json = loadResourceJsonFile("testComplexCaseWithMultipleOrAndExpressions2"); 226 | 227 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 228 | 229 | List employeeList = employeeRepository.findAll(expressions); 230 | assertThat(employeeList).isNotNull(); 231 | assertThat(employeeList.size()).isEqualTo(2); 232 | // where last_name = ? and (first_name = ? or birth_date < ?) 233 | } 234 | 235 | @Test 236 | public void testUsingTheBuilderMethodOfExpressionsWithOr() throws Exception { 237 | 238 | String json = loadResourceJsonFile("testUsingTheBuilderMethodOfExpressionsWithOr"); 239 | 240 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 241 | 242 | expressions.and(Expression.or( 243 | Expression.of("lastName", $eq, "ibrahim"), 244 | Expression.of("age", $in, 10, 30) 245 | )); 246 | 247 | List employeeList = employeeRepository.findAll(expressions); 248 | assertThat(employeeList).isNotNull(); 249 | assertThat(employeeList.size()).isEqualTo(3); 250 | 251 | // where hire_date employeeList = employeeRepository.findAll(expressions); 267 | assertThat(employeeList).isNotNull(); 268 | assertThat(employeeList.size()).isEqualTo(3); 269 | 270 | // where hire_date employeeList = employeeRepository.findAll(expressions); 281 | assertThat(employeeList).isNotNull(); 282 | assertThat(employeeList.size()).isEqualTo(6); 283 | 284 | // where ?=1 285 | } 286 | 287 | @Test 288 | public void testSearchByEmptyObjectAndWithDatesFromJava() throws Exception { 289 | 290 | String json = "{}"; 291 | 292 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 293 | 294 | expressions.and(Expression.of("birthDate", $eq, "1980-10-10")); 295 | expressions.and(Expression.of("hireDate", $lte, "2007-12-03T10:15:30.00Z")); 296 | 297 | List employeeList = employeeRepository.findAll(expressions); 298 | assertThat(employeeList).isNotNull(); 299 | assertThat(employeeList.size()).isEqualTo(1); 300 | assertThat(employeeList.get(0).firstName).isEqualTo("ahmed"); 301 | 302 | // where employee0_.hire_date=? and employee0_.birth_date=? 303 | } 304 | 305 | @Test 306 | public void testSearchByNull() throws Exception { 307 | 308 | String json = loadResourceJsonFile("testSearchByNull"); 309 | 310 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 311 | 312 | expressions.or(Expression.of("lastName", $ne, (String) null)); 313 | 314 | List employeeList = employeeRepository.findAll(expressions); 315 | assertThat(employeeList).isNotNull(); 316 | assertThat(employeeList.size()).isEqualTo(6); 317 | 318 | // where employee0_.first_name is null or employee0_.last_name is not null 319 | } 320 | 321 | @Test 322 | public void testSearchByContains() throws Exception { 323 | 324 | String json = loadResourceJsonFile("testSearchByContains"); 325 | 326 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 327 | 328 | List employeeList = employeeRepository.findAll(expressions); 329 | assertThat(employeeList).isNotNull(); 330 | assertThat(employeeList.size()).isEqualTo(3); 331 | 332 | // where employee0_.last_name like ? 333 | } 334 | 335 | @Test 336 | public void testSearchByIgnoreCaseContains() throws Exception { 337 | 338 | String json = loadResourceJsonFile("testSearchByIgnoreCaseContains"); 339 | 340 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 341 | 342 | List employeeList = employeeRepository.findAll(expressions); 343 | assertThat(employeeList).isNotNull(); 344 | assertThat(employeeList.size()).isEqualTo(3); 345 | 346 | // where lower(employee0_.last_name) like ? 347 | } 348 | 349 | @Test 350 | public void testOperatorNotSupported() throws Exception { 351 | 352 | String json = loadResourceJsonFile("testOperatorNotSupported"); 353 | 354 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 355 | 356 | try { 357 | employeeRepository.findAll(expressions); 358 | 359 | fail("should throw exception"); 360 | } catch (Exception ex) { 361 | assertThat(ex.getMessage()) 362 | .contains("No enum constant") 363 | .contains("Operator.$not_supported_operator"); 364 | } 365 | } 366 | 367 | @Test 368 | public void testInvalidFieldName() throws Exception { 369 | 370 | String json = loadResourceJsonFile("testInvalidFieldName"); 371 | 372 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 373 | 374 | try { 375 | employeeRepository.findAll(expressions); 376 | 377 | fail("should throw exception"); 378 | } catch (Exception ex) { 379 | assertThat(ex.getMessage()) 380 | .contains("invalidFieldName") 381 | .contains("com.github.mhewedy.expressions.model.Employee"); 382 | } 383 | } 384 | 385 | @Test 386 | public void testTheJavaAPI() { 387 | Expressions expressions = Expression.of("lastName", $eq, "ibrahim") 388 | .and(Expression.or( 389 | Expression.of("age", $in, 10, 20), 390 | Expression.of("birthDate", $lt, LocalDate.of(1980, 1, 1))) 391 | ).build(); 392 | 393 | List employeeList = employeeRepository.findAll(expressions); 394 | assertThat(employeeList).isNotNull(); 395 | assertThat(employeeList.size()).isEqualTo(2); 396 | 397 | // where last_name=? and (age in (? , ?) or birth_date employeeList = employeeRepository.findAll(expressions); 407 | assertThat(employeeList).isNotNull(); 408 | assertThat(employeeList.size()).isEqualTo(6); 409 | 410 | // where type in ? or active=? 411 | } 412 | 413 | @Test 414 | public void testNestingUsingManyToOneJoin() throws Exception { 415 | String json = loadResourceJsonFile("testNestingUsingManyToOneJoin"); 416 | 417 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 418 | 419 | List employeeList = employeeRepository.findAll(expressions); 420 | assertThat(employeeList).isNotNull(); 421 | assertThat(employeeList.size()).isEqualTo(1); 422 | 423 | // from employee e inner join department d on e.department_id=d.id where e.last_name=? and (d.name like ?) 424 | } 425 | 426 | @Test 427 | public void testNestingUsingManyToOneJoinUsingInQueries() throws Exception { 428 | String json = loadResourceJsonFile("testNestingUsingManyToOneJoinUsingInQueries"); 429 | 430 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 431 | 432 | List employeeList = employeeRepository.findAll(expressions); 433 | assertThat(employeeList).isNotNull(); 434 | assertThat(employeeList.size()).isEqualTo(4); 435 | 436 | // employee e inner join department d on e.department_id=d.id where d.name in (? , ?) 437 | } 438 | 439 | @Test 440 | public void testNestingUsingManyToOneJoinUsingDeepNestedLevel() throws Exception { 441 | String json = loadResourceJsonFile("testNestingUsingManyToOneJoinUsingDeepNestedLevel"); 442 | 443 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 444 | 445 | List employeeList = employeeRepository.findAll(expressions); 446 | assertThat(employeeList).isNotNull(); 447 | assertThat(employeeList.size()).isEqualTo(2); 448 | 449 | // from employee e inner join department d on e.department_id=d.id inner join city c on d.city_id=c.id where e.last_name=? and c.name=? 450 | } 451 | 452 | @Test 453 | public void testNestingUsingOneToManyJoin() throws Exception { 454 | String json = loadResourceJsonFile("testNestingUsingOneToManyJoin"); 455 | 456 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 457 | 458 | List employeeList = employeeRepository.findAll(expressions); 459 | assertThat(employeeList).isNotNull(); 460 | assertThat(employeeList.size()).isEqualTo(5); 461 | 462 | // from employee e inner join task t on e.id=t.employee_id where t.name like ? 463 | } 464 | 465 | @Test 466 | public void testNestingUsingManyToOneJoinWithMultipleFields() throws Exception { 467 | String json = loadResourceJsonFile("testNestingUsingManyToOneJoinWithMultipleFields"); 468 | 469 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 470 | 471 | List employeeList = employeeRepository.findAll(expressions); 472 | assertThat(employeeList).isNotNull(); 473 | assertThat(employeeList.size()).isEqualTo(0); 474 | 475 | // from employee e inner join department d on e.department_id=d.id where e.last_name=? and d.id=? and d.name=? 476 | } 477 | 478 | @Test 479 | public void testNestingUsingManyToOneJoinWithMultipleFields_Advanced() throws Exception { 480 | String json = loadResourceJsonFile("testNestingUsingManyToOneJoinWithMultipleFields_Advanced"); 481 | 482 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 483 | 484 | List employeeList = employeeRepository.findAll(expressions); 485 | assertThat(employeeList).isNotNull(); 486 | assertThat(employeeList.size()).isEqualTo(2); 487 | 488 | // from employee e inner join department d on e.department_id=d.id inner join city c on d.city_id=c.id 489 | // where e.last_name=? and c.name=? and (d.name in (? , ?)) 490 | } 491 | 492 | @Test 493 | public void testEmbeddedAndJoin() throws Exception { 494 | String json = loadResourceJsonFile("testEmbeddedAndJoin"); 495 | 496 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 497 | 498 | List employeeList = employeeRepository.findAll(expressions); 499 | assertThat(employeeList).isNotNull(); 500 | assertThat(employeeList.size()).isEqualTo(1); 501 | 502 | // from employee e inner join department d on e.department_id=d.id where e.employee_name_ar=? and d.name=? 503 | } 504 | 505 | @Test 506 | public void testEmbeddedInAndJoin() throws Exception { 507 | String json = loadResourceJsonFile("testEmbeddedInAndJoin"); 508 | 509 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 510 | 511 | List employeeList = employeeRepository.findAll(expressions); 512 | assertThat(employeeList).isNotNull(); 513 | assertThat(employeeList.size()).isEqualTo(2); 514 | 515 | // from employee e inner join department d on e.department_id=d.id where (e.employee_name_ar in (? , ?)) and d.name=? 516 | } 517 | 518 | @Test 519 | public void testNumberContains() throws Exception { 520 | String json = loadResourceJsonFile("testNumberContains"); 521 | 522 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 523 | 524 | List employeeList = employeeRepository.findAll(expressions); 525 | assertThat(employeeList).isNotNull(); 526 | assertThat(employeeList.size()).isEqualTo(6); 527 | 528 | // from employee e where cast(e.age as varchar(255)) like ? 529 | } 530 | 531 | @Test 532 | public void testNumberContains_Count() throws Exception { 533 | String json = loadResourceJsonFile("testNumberContains"); 534 | 535 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 536 | 537 | long count = employeeRepository.count(expressions); 538 | assertThat(count).isEqualTo(6); 539 | 540 | // from employee e where cast(e.age as varchar(255)) like ? 541 | } 542 | 543 | @Test 544 | public void testEnumInInts() throws Exception { 545 | String json = loadResourceJsonFile("testEnumInInts"); 546 | 547 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 548 | 549 | List employeeList = employeeRepository.findAll(expressions); 550 | assertThat(employeeList.size()).isEqualTo(5); 551 | 552 | // from employee e inner join task t on e.id=t.employee_id where t.status in (? , ?) 553 | } 554 | 555 | @Test 556 | public void testEnumNotInStrings() throws Exception { 557 | String json = loadResourceJsonFile("testEnumNotInStrings"); 558 | 559 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 560 | 561 | List employeeList = employeeRepository.findAll(expressions); 562 | assertThat(employeeList.size()).isEqualTo(1); 563 | 564 | // from employee e inner join task t on e.id=t.employee_id where t.status not in (?) 565 | } 566 | 567 | @Test 568 | public void testManyToOneIsNull() throws Exception { 569 | String json = loadResourceJsonFile("testManyToOneIsNull"); 570 | 571 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 572 | 573 | List employeeList = employeeRepository.findAll(expressions); 574 | assertThat(employeeList.size()).isEqualTo(1); 575 | 576 | // from employee e where e.department is null 577 | } 578 | 579 | @Test 580 | public void testBooleanOperatorFromJava() { 581 | Expressions expressions = Expression.of("active", $eq, false).build(); 582 | 583 | List employeeList = employeeRepository.findAll(expressions); 584 | assertThat(employeeList.size()).isEqualTo(2); 585 | 586 | // where e.active=? 587 | } 588 | 589 | @Test 590 | public void testUUID() throws Exception { 591 | String json = loadResourceJsonFile("testUUID"); 592 | 593 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 594 | 595 | List employeeList = employeeRepository.findAll(expressions); 596 | assertThat(employeeList.size()).isEqualTo(1); 597 | 598 | // where e.serial=? 599 | } 600 | 601 | @Test 602 | public void testHijrahDate() throws Exception { 603 | String json = loadResourceJsonFile("testHijrahDate"); 604 | 605 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 606 | 607 | List employeeList = employeeRepository.findAll(expressions); 608 | assertThat(employeeList.size()).isEqualTo(1); 609 | 610 | // where e.h_birth_date>=? 611 | } 612 | 613 | @Test 614 | public void testHijrahDate_InJava() { 615 | Expressions expressions = Expression.of("hBirthDate", $gte, HijrahDate.of(1390, 9, 29)).build(); 616 | 617 | List employeeList = employeeRepository.findAll(expressions); 618 | assertThat(employeeList.size()).isEqualTo(1); 619 | 620 | // where e.h_birth_date>=? 621 | } 622 | 623 | @Test 624 | public void testCompositeIdUsingEmbeddable() throws Exception { 625 | String json = loadResourceJsonFile("testCompositeIdUsingEmbeddable"); 626 | 627 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 628 | 629 | List employeeList = bookRepository.findAll(expressions); 630 | assertThat(employeeList.size()).isEqualTo(1); 631 | 632 | // where b1_0.auther=? 633 | } 634 | 635 | @Test 636 | public void testCompositeIdUsingEmbeddable_QueryByIdParts() throws Exception { 637 | String json = loadResourceJsonFile("testCompositeIdUsingEmbeddable_QueryByIdParts"); 638 | 639 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 640 | 641 | List employeeList = bookRepository.findAll(expressions); 642 | assertThat(employeeList.size()).isEqualTo(1); 643 | 644 | // where b1_0.title=? 645 | } 646 | 647 | @Test 648 | public void testCompositeIdUsingEmbeddable_QueryByIdParts2() throws Exception { 649 | String json = loadResourceJsonFile("testCompositeIdUsingEmbeddable_QueryByIdParts2"); 650 | 651 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 652 | 653 | List employeeList = bookRepository.findAll(expressions); 654 | assertThat(employeeList.size()).isEqualTo(2); 655 | 656 | // where b1_0.language=? 657 | } 658 | 659 | @Test 660 | public void testAggregations_by_convert_to_specifications_InJava() { 661 | Expressions expressions = Expression.of("hBirthDate", Operator.$gte, HijrahDate.of(1388, 9, 29)).build(); 662 | 663 | // get spring-data-jpa Specification from the expressions object 664 | Specification specification = expressions.getSpecification(); 665 | 666 | // then using old school jpa Criteria Query API 667 | CriteriaBuilder cb = entityManager.getCriteriaBuilder(); 668 | CriteriaQuery query = cb.createQuery(Integer.class); 669 | Root root = query.from(Employee.class); 670 | query.select(cb.min(root.get("age"))).where(specification.toPredicate(root, query, cb)); 671 | Integer minAge = entityManager.createQuery(query).getSingleResult(); 672 | 673 | assertThat(minAge).isEqualTo(30); 674 | 675 | // select min(e1_0.age) from employee e1_0 where e1_0.h_birth_date>=? 676 | } 677 | 678 | @Test 679 | public void testAndingMultipleOrs_InJava() { 680 | 681 | Expressions expressions = new Expressions(); 682 | 683 | expressions.and(Expression.or( 684 | Expression.of("age", $eq, 30), 685 | Expression.of("age", $eq, 50) 686 | )); 687 | 688 | expressions.and(Expression.or( 689 | Expression.of("firstName", $eq, "ali"), 690 | Expression.of("firstName", $eq, "wael") 691 | )); 692 | 693 | List employeeList = employeeRepository.findAll(expressions); 694 | assertThat(employeeList.size()).isEqualTo(0); 695 | 696 | // where (employee0_.age=? or employee0_.age=?) and (employee0_.first_name=? or employee0_.first_name=?) 697 | } 698 | 699 | @Test 700 | public void testEnum1() { 701 | Expressions expressions = new Expressions(); 702 | expressions.and(Expression.of("lang", $eq, Lang.EN)); 703 | List employeeList = employeeRepository.findAll(expressions); 704 | assertThat(employeeList.size()).isEqualTo(1); 705 | } 706 | 707 | @Test 708 | public void testEnum2() { 709 | Expressions expressions = new Expressions(); 710 | expressions.and(Expression.of("langCode", $eq, Lang.EN)); 711 | List employeeList = employeeRepository.findAll(expressions); 712 | assertThat(employeeList.size()).isEqualTo(1); 713 | } 714 | 715 | @Test 716 | public void testEnum3() { 717 | Expressions expressions = new Expressions(); 718 | expressions.and(Expression.of("langCode", $in, Lang.EN, Lang.AR)); 719 | List employeeList = employeeRepository.findAll(expressions); 720 | assertThat(employeeList.size()).isEqualTo(allEmployees.size()); 721 | } 722 | 723 | @Test 724 | public void testTheList() { 725 | Expressions expressions = Expression.of("lastName", $eq, "ibrahim") 726 | .and(Expression.or( 727 | Expression.of("age", $in, Arrays.asList(10, 20)), 728 | Expression.of("birthDate", $lt, LocalDate.of(1980, 1, 1))) 729 | ).build(); 730 | 731 | List employeeList = employeeRepository.findAll(expressions); 732 | assertThat(employeeList).isNotNull(); 733 | assertThat(employeeList.size()).isEqualTo(2); 734 | 735 | // where last_name=? and (age in (? , ?) or birth_date employeeList = employeeRepository.findAll(expressions); 745 | assertThat(employeeList.size()).isEqualTo(3); 746 | 747 | // ...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ? 748 | } 749 | 750 | @Test 751 | public void testLeftJoinAlternateSyntax() throws JsonProcessingException { 752 | String json = loadResourceJsonFile("testLeftJoinAlternateSyntax"); 753 | 754 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 755 | 756 | List employeeList = employeeRepository.findAll(expressions); 757 | assertThat(employeeList.size()).isEqualTo(3); 758 | 759 | // ...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ? 760 | } 761 | 762 | @Test 763 | public void testSearchWithMultipleNestedJoins() throws Exception { 764 | String json = """ 765 | { 766 | "$and": [ 767 | { 768 | "department.city.name": "cairo" 769 | }, 770 | { 771 | "tasks.status": "ACTIVE" 772 | } 773 | ] 774 | } 775 | """; 776 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 777 | List employees = employeeRepository.findAll(expressions); 778 | 779 | assertThat(employees).isNotNull(); 780 | assertThatList(employees).hasSize(3); // All employees in Cairo with active tasks 781 | assertThatList(employees).allSatisfy(emp -> { 782 | assertThat(emp.department).isNotNull(); 783 | assertThat(emp.department.city).isNotNull(); 784 | assertThat(emp.department.city.name).isEqualTo("cairo"); 785 | assertThat(emp.tasks).isNotNull(); 786 | assertThat(emp.tasks.stream().anyMatch(task -> task.status == ACTIVE)).isTrue(); 787 | }); 788 | } 789 | 790 | @Test 791 | public void testSearchWithComplexNestedConditions() throws Exception { 792 | String json = """ 793 | { 794 | "$or": [ 795 | { 796 | "department.name": "hr", 797 | "tasks.status": "ACTIVE" 798 | }, 799 | { 800 | "department.name": "sw dev", 801 | "age": { "$gt": 25 } 802 | } 803 | ] 804 | } 805 | """; 806 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 807 | List employees = employeeRepository.findAll(expressions); 808 | 809 | assertThat(employees).isNotNull(); 810 | assertThatList(employees).hasSize(4); // HR employees with active tasks + SW dev employees over 25 811 | assertThatList(employees).allSatisfy(emp -> { 812 | if (emp.department != null && emp.department.name.equals("hr")) { 813 | assertThat(emp.tasks).isNotNull(); 814 | assertThat(emp.tasks.stream().anyMatch(task -> task.status == ACTIVE)).isTrue(); 815 | } else if (emp.department != null && emp.department.name.equals("sw dev")) { 816 | assertThat(emp.age).isGreaterThan(25); 817 | } 818 | }); 819 | } 820 | 821 | @Test 822 | public void testSearchWithMultipleSortOrders() throws Exception { 823 | String json = """ 824 | { 825 | "department.name": "hr" 826 | } 827 | """; 828 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 829 | Sort sort = Sort.by( 830 | Sort.Order.asc("firstName"), 831 | Sort.Order.desc("age") 832 | ); 833 | List employees = employeeRepository.findAll(expressions, sort); 834 | 835 | assertThat(employees).isNotNull(); 836 | assertThatList(employees).hasSize(3); // All HR employees 837 | assertThatList(employees).allSatisfy(emp -> { 838 | assertThat(emp.department).isNotNull(); 839 | assertThat(emp.department.name).isEqualTo("hr"); 840 | }); 841 | 842 | // Verify sorting 843 | for (int i = 1; i < employees.size(); i++) { 844 | Employee prev = employees.get(i-1); 845 | Employee curr = employees.get(i); 846 | assertThat(prev.firstName.compareTo(curr.firstName)).isLessThanOrEqualTo(0); 847 | if (prev.firstName.equals(curr.firstName)) { 848 | assertThat(prev.age).isGreaterThanOrEqualTo(curr.age); 849 | } 850 | } 851 | } 852 | 853 | @Test 854 | public void testSearchWithPaginationAndComplexConditions() throws Exception { 855 | String json = """ 856 | { 857 | "$or": [ 858 | { 859 | "department.name": "hr" 860 | }, 861 | { 862 | "department.name": "sw dev" 863 | } 864 | ], 865 | "age": { "$gte": 30 } 866 | } 867 | """; 868 | Expressions expressions = new ObjectMapper().readValue(json, Expressions.class); 869 | Page page = employeeRepository.findAll(expressions, PageRequest.of(0, 2, Sort.by("firstName"))); 870 | 871 | assertThat(page).isNotNull(); 872 | assertThat(page.getTotalElements()).isEqualTo(3); // Total matching employees 873 | assertThat(page.getSize()).isEqualTo(2); // Page size 874 | assertThat(page.getContent()).isNotNull(); 875 | assertThatList(page.getContent()).hasSize(2); // Current page content 876 | assertThatList(page.getContent()).allSatisfy(emp -> { 877 | assertThat(emp.department).isNotNull(); 878 | assertThat(emp.department.name).isIn("hr", "sw dev"); 879 | assertThat(emp.age).isGreaterThanOrEqualTo(30); 880 | }); 881 | } 882 | 883 | @SneakyThrows 884 | private String loadResourceJsonFile(String name) { 885 | File file = ResourceUtils.getFile("classpath:" + name + ".json"); 886 | return new String(Files.readAllBytes(file.toPath())); 887 | } 888 | } 889 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/ExpressionsTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.github.mhewedy.expressions; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import static com.github.mhewedy.expressions.Expression.*; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatList; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | 18 | class ExpressionsTest { 19 | 20 | private final ObjectMapper objectMapper = new ObjectMapper(); 21 | 22 | @Test 23 | public void testObjectWithOrExpression() throws Exception { 24 | 25 | Expressions conditions = 26 | objectMapper.readValue(""" 27 | { 28 | "status": "A", 29 | "$or": [ 30 | { 31 | "qty": { "$lt": 30 } 32 | }, 33 | { 34 | "item": {"$in": ["A", "D"] } 35 | } 36 | ] 37 | } 38 | """, 39 | Expressions.class); 40 | 41 | final List expression = conditions.getExpressions(); 42 | 43 | assertThat(expression).hasSize(2); 44 | 45 | assertThat(expression).hasAtLeastOneElementOfType(OrExpression.class); 46 | assertThat(expression).hasAtLeastOneElementOfType(SingularExpression.class); 47 | 48 | for (Expression element : expression) { 49 | if (element instanceof OrExpression) { 50 | OrExpression expr = (OrExpression) element; 51 | assertThat(expr.expressions).hasSize(2); 52 | 53 | assertThat(expr.expressions).hasAtLeastOneElementOfType(ListExpression.class); 54 | assertThat(expr.expressions).hasAtLeastOneElementOfType(SingularExpression.class); 55 | 56 | for (Expression ee : expr.expressions) { 57 | 58 | if (ee instanceof SingularExpression) { 59 | 60 | SingularExpression ees = (SingularExpression) ee; 61 | 62 | assertThat(ees.field).isEqualTo("qty"); 63 | assertThat(ees.operator).isEqualTo(Operator.$lt); 64 | assertThat(ees.value).isEqualTo(30); 65 | } 66 | 67 | if (ee instanceof ListExpression) { 68 | 69 | ListExpression ees = (ListExpression) ee; 70 | 71 | assertThat(ees.field).isEqualTo("item"); 72 | assertThat(ees.operator).isEqualTo(Operator.$in); 73 | assertThat(ees.values).hasSize(2); 74 | assertThat(ees.values).containsExactlyInAnyOrder("A", "D"); 75 | } 76 | } 77 | } 78 | 79 | if (element instanceof SingularExpression) { 80 | SingularExpression expr = (SingularExpression) element; 81 | assertThat(expr.field).isEqualTo("status"); 82 | assertThat(expr.operator).isEqualTo(Operator.$eq); 83 | assertThat(expr.value).isEqualTo("A"); 84 | } 85 | } 86 | } 87 | 88 | @Test 89 | void testExtractFieldsWithCompoundOperatorsJSON() throws Exception { 90 | String json = """ 91 | { 92 | "firstName": "John", 93 | "$or": [ 94 | { 95 | "lastName": "Doe" 96 | }, 97 | { 98 | "age": { "$gt": 30 } 99 | } 100 | ] 101 | } 102 | """; 103 | Expressions expressions = objectMapper.readValue(json, Expressions.class); 104 | Set fields = expressions.extractFields().keySet(); 105 | 106 | assertEquals(Set.of("firstName", "lastName", "age"), fields); 107 | } 108 | 109 | @Test 110 | void testExtractFieldsWithEmptyJSON() throws Exception { 111 | String json = "{}"; 112 | Expressions expressions = objectMapper.readValue(json, Expressions.class); 113 | Set fields = expressions.extractFields().keySet(); 114 | 115 | assertTrue(fields.isEmpty()); 116 | } 117 | 118 | @Test 119 | void testExtractFieldsWithNestedAndOperatorJSON() throws Exception { 120 | String json = """ 121 | { 122 | "$and": [ 123 | { "country": "USA" }, 124 | { "state": "California" } 125 | ] 126 | } 127 | """; 128 | Expressions expressions = objectMapper.readValue(json, Expressions.class); 129 | Set fields = expressions.extractFields().keySet(); 130 | 131 | assertEquals(Set.of("country", "state"), fields); 132 | } 133 | 134 | @Test 135 | void testExtractFieldsWithMultipleNestedOperatorsJSON() throws Exception { 136 | String json = """ 137 | { 138 | "$and": [ 139 | { 140 | "$or": [ 141 | { "city": "New York" }, 142 | { "zipcode": "10001" } 143 | ] 144 | }, 145 | { 146 | "city.country": "USA" 147 | } 148 | ] 149 | } 150 | """; 151 | Expressions expressions = objectMapper.readValue(json, Expressions.class); 152 | Set fields = expressions.extractFields().keySet(); 153 | List values = expressions.extractFields().values().stream().toList(); 154 | 155 | assertEquals(Set.of("city", "zipcode", "city.country"), fields); 156 | assertThatList(values).containsExactlyInAnyOrder("New York", "10001", "USA"); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/HijrahDateConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import jakarta.persistence.AttributeConverter; 6 | import jakarta.persistence.Converter; 7 | import java.time.chrono.HijrahDate; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | @Slf4j 11 | @Converter(autoApply = true) 12 | public class HijrahDateConverter implements AttributeConverter { 13 | 14 | private static final DateTimeFormatter BASIC_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); 15 | 16 | @Override 17 | public Integer convertToDatabaseColumn(HijrahDate date) { 18 | return formatHijriAsInteger(date); 19 | } 20 | 21 | @Override 22 | public HijrahDate convertToEntityAttribute(Integer dbDate) { 23 | return parseHijriDate(dbDate); 24 | } 25 | 26 | private static Integer formatHijriAsInteger(HijrahDate hijrahDate) { 27 | if (hijrahDate == null) { 28 | return null; 29 | } 30 | return Integer.parseInt(hijrahDate.format(BASIC_DATE_FORMAT)); 31 | } 32 | 33 | private static HijrahDate parseHijriDate(Integer date) { 34 | String d; 35 | if (date == null || date == 0 || (d = String.valueOf(date)).length() != 8) { 36 | return null; 37 | } 38 | return HijrahDate.of( 39 | Integer.parseInt(d.substring(0, 4)), 40 | Integer.parseInt(d.substring(4, 6)), 41 | Integer.parseInt(d.substring(6, 8)) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Auditable.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import jakarta.persistence.MappedSuperclass; 4 | import java.time.Instant; 5 | 6 | @MappedSuperclass 7 | public class Auditable { 8 | 9 | public Instant createdDate; 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Book.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import jakarta.persistence.Embeddable; 4 | import jakarta.persistence.EmbeddedId; 5 | import jakarta.persistence.Entity; 6 | import lombok.AllArgsConstructor; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class Book { 15 | @EmbeddedId 16 | public BookId id; 17 | 18 | public String auther; 19 | 20 | @Embeddable 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | public static class BookId implements Serializable { 24 | public String title; 25 | public String language; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/City.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | 10 | @Entity 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class City { 14 | 15 | @Id 16 | @GeneratedValue 17 | public Long id; 18 | public String name; 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Department.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Entity 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class Department { 12 | 13 | @Id 14 | @GeneratedValue 15 | public Long id; 16 | public String name; 17 | @OneToOne(cascade = CascadeType.PERSIST) 18 | public City city; 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Employee.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.Instant; 8 | import java.time.LocalDate; 9 | import java.time.chrono.HijrahDate; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @Entity 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class Employee extends Auditable { 17 | @Id 18 | @GeneratedValue 19 | public Long id; 20 | 21 | public String firstName; 22 | public String lastName; 23 | @Embedded 24 | @AttributeOverrides({ 25 | @AttributeOverride(name = "ar", column = @Column(name = "employee_name_ar")), 26 | @AttributeOverride(name = "en", column = @Column(name = "employee_name_en")) 27 | }) 28 | public LingualString name; 29 | public LocalDate birthDate; 30 | public HijrahDate hBirthDate; 31 | public Integer age; 32 | public Instant hireDate; 33 | public Short type; 34 | public Boolean active; 35 | @ManyToOne(cascade = CascadeType.PERSIST) 36 | public Department department; 37 | @OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) 38 | @JoinColumn(name = "employee_id") 39 | public List tasks; 40 | public UUID serial; 41 | @Enumerated(EnumType.STRING) 42 | public Lang lang; 43 | @Enumerated(EnumType.ORDINAL) 44 | public Lang langCode; 45 | 46 | public enum Lang {AR, EN} 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/LingualString.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | @Embeddable 6 | public class LingualString { 7 | public String ar, en; 8 | 9 | public LingualString() { 10 | } 11 | 12 | public LingualString(String ar, String en) { 13 | this.ar = ar; 14 | this.en = en; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Status.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | public enum Status { 4 | ACTIVE, NOT_ACTIVE 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/com/github/mhewedy/expressions/model/Task.java: -------------------------------------------------------------------------------- 1 | package com.github.mhewedy.expressions.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | 10 | @Entity 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class Task { 14 | 15 | @Id 16 | @GeneratedValue 17 | public Long id; 18 | public String name; 19 | public Status status; 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | 'hibernate.criteria.literal_handling_mode': bind 5 | 6 | logging: 7 | level: 8 | com.github.mhewedy.expressions: debug 9 | org.hibernate.type.descriptor.sql.BasicBinder: trace -------------------------------------------------------------------------------- /src/test/resources/testComplexCaseWithMultipleOrAndExpressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$or": [ 3 | { 4 | "lastName": "ibrahim" 5 | }, 6 | { 7 | "$and": [ 8 | { 9 | "firstName": "mostafa" 10 | }, 11 | { 12 | "birthDate": { 13 | "$gt": "1990-01-01" 14 | } 15 | } 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/test/resources/testComplexCaseWithMultipleOrAndExpressions2.json: -------------------------------------------------------------------------------- 1 | { 2 | "$and": [ 3 | { 4 | "lastName": "ibrahim" 5 | }, 6 | { 7 | "$or": [ 8 | { 9 | "firstName": "ahmed" 10 | }, 11 | { 12 | "birthDate": { 13 | "$lt": "1990-01-01" 14 | } 15 | } 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/test/resources/testCompositeIdUsingEmbeddable.json: -------------------------------------------------------------------------------- 1 | { 2 | "auther": "Craig Walls" 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/testCompositeIdUsingEmbeddable_QueryByIdParts.json: -------------------------------------------------------------------------------- 1 | { 2 | "id.title": "Spring in Action" 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/testCompositeIdUsingEmbeddable_QueryByIdParts2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id.language": "English" 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/testEmbeddedAndJoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name.ar": "ahmed ar", 3 | "department.name": "hr" 4 | } -------------------------------------------------------------------------------- /src/test/resources/testEmbeddedInAndJoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name.ar": {"$in": ["ahmed ar", "wael ar"]}, 3 | "department.name": "hr" 4 | } -------------------------------------------------------------------------------- /src/test/resources/testEnumInInts.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks.status": {"$in": [0, 1]} 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/testEnumNotInStrings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks.status": {"$nin": ["ACTIVE"]} 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/testFindAllInBaseRepository.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastName": "ibrahim", 3 | "$and": [ 4 | { 5 | "birthDate": {"$gt": "1981-01-01"} 6 | }, 7 | { 8 | "birthDate": {"$lte": "1985-10-10"} 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/test/resources/testHijrahDate.json: -------------------------------------------------------------------------------- 1 | { 2 | "hBirthDate": { 3 | "$gte": "1390-09-29" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/testInvalidFieldName.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalidFieldName": "rah" 3 | } -------------------------------------------------------------------------------- /src/test/resources/testLeftJoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$or": [ 3 | { 4 | "firstName": "farida" 5 | }, 6 | { 7 | "department.city?.name": "cairo" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /src/test/resources/testLeftJoinAlternateSyntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "$or": [ 3 | { 4 | "firstName": "farida" 5 | }, 6 | { 7 | "department.