├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── new-feature-request.md ├── PULL_REQUEST_TEMPLATE └── workflows │ ├── pr.yml │ └── spring-search-deploy.yml ├── .gitignore ├── .java-version ├── .jpb └── jpb-settings.xml ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── catalog-info.yaml ├── docs └── images │ ├── and-example.gif │ ├── complex-example.gif │ ├── deep-field-example.gif │ ├── equal-example.gif │ ├── greater-than-example.gif │ ├── less-than-example.gif │ ├── not-equal-example.gif │ ├── or-example.gif │ ├── parenthesis-example.gif │ ├── space-example.gif │ ├── special-characters-example.gif │ └── starts-with-example.gif ├── mvnw ├── mvnw.cmd ├── pom.xml ├── scripts └── bump_project_version.sh └── src ├── main ├── antlr4 │ └── Query.g4 ├── kotlin │ └── com │ │ └── sipios │ │ └── springsearch │ │ ├── CriteriaParser.kt │ │ ├── QueryVisitorImpl.kt │ │ ├── SearchCriteria.kt │ │ ├── SearchOperation.kt │ │ ├── SpecificationImpl.kt │ │ ├── SpecificationsBuilder.kt │ │ ├── anotation │ │ └── SearchSpec.kt │ │ ├── configuration │ │ ├── ResolverConf.kt │ │ └── SearchSpecificationResolver.kt │ │ ├── listeners │ │ └── SyntaxErrorListener.kt │ │ └── strategies │ │ ├── BooleanStrategy.kt │ │ ├── CollectionStrategy.kt │ │ ├── DateStrategy.kt │ │ ├── DoubleStrategy.kt │ │ ├── DurationStrategy.kt │ │ ├── EnumStrategy.kt │ │ ├── FloatStrategy.kt │ │ ├── InstantStrategy.kt │ │ ├── IntStrategy.kt │ │ ├── LocalDateStrategy.kt │ │ ├── LocalDateTimeStrategy.kt │ │ ├── LocalTimeStrategy.kt │ │ ├── ParsingStrategy.kt │ │ ├── StringStrategy.kt │ │ └── UUIDStrategy.kt └── resources │ ├── META-INF │ └── spring.factories │ └── application.properties └── test ├── kotlin └── com │ └── sipios │ └── springsearch │ ├── Author.kt │ ├── AuthorRepository.kt │ ├── Book.kt │ ├── SpringSearchApplication.kt │ ├── SpringSearchApplicationTest.kt │ ├── UserType.kt │ ├── Users.kt │ └── UsersRepository.kt └── resources └── application.properties /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Create a bug report to help us improve 4 | title: '' 5 | labels: bug, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | 13 | 14 | 15 | # Standard debugging steps 16 | 17 | ## How to reproduce? 18 | 19 | 20 | 21 | ## Expected behavior 22 | 23 | 24 | 25 | **Screenshots**: 26 | 27 | 28 | ## Stacktrace 29 | 30 | 31 | 32 | ``` 33 | paste logs here 34 | ``` 35 | 36 | 37 | # Other details 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New feature request 🚀 3 | about: Suggest a new idea 4 | title: '' 5 | labels: needs triage, new change 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | 13 | 14 | 15 | # Background 16 | 17 | **Is your feature request related to a problem? Please describe**: 18 | 19 | 20 | **Describe the solution you'd like**: 21 | 22 | 23 | **Describe alternatives you've considered**: 24 | 25 | 26 | 27 | # Details 28 | 29 | 30 | 31 | 32 | # Outcome 33 | 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/sipios/spring-search//blob/main/CONTRIBUTING.md##Commit-messages 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | 14 | - [ ] Bugfix 15 | - [ ] Feature 16 | - [ ] Code style update (formatting, local variables) 17 | - [ ] Refactoring (no functional changes, no api changes) 18 | - [ ] Build related changes 19 | - [ ] CI related changes 20 | - [ ] Documentation content changes 21 | - [ ] Other... Please describe: 22 | 23 | 24 | ## What is the current behavior? 25 | 26 | 27 | Issue Number: N/A 28 | 29 | 30 | ## What is the new behavior? 31 | 32 | 33 | ## Does this PR introduce a breaking change? 34 | 35 | - [ ] Yes 36 | - [ ] No 37 | 38 | 39 | 40 | 41 | 42 | ## Other information -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Test & Coverage 2 | on: 3 | pull_request: 4 | branches: 5 | - '*' 6 | 7 | jobs: 8 | test-and-coverage: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '17' 18 | distribution: 'temurin' 19 | 20 | - name: Set up the Maven dependencies caching 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/.m2 24 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 25 | restore-keys: ${{ runner.os }}-m2 26 | 27 | - name: Run tests 28 | run: mvn --batch-mode --update-snapshots verify -Dgpg.skip=true -Ddependency-check.skip=true 29 | 30 | - name: Add coverage 31 | uses: madrapps/jacoco-report@v1.6.1 32 | with: 33 | paths: | 34 | ${{ github.workspace }}/target/test-results/coverage/jacoco/jacoco.xml 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | title: '### :zap: Coverage report' 37 | update-comment: true 38 | min-coverage-overall: 80 39 | min-coverage-changed-files: 75 40 | continue-on-error: false 41 | -------------------------------------------------------------------------------- /.github/workflows/spring-search-deploy.yml: -------------------------------------------------------------------------------- 1 | name: SpringSearch Deploy 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | branches: 8 | - master 9 | 10 | env: 11 | TAG_NAME: ${{ github.event.pull_request.title }} 12 | 13 | jobs: 14 | release-maven-central: 15 | if: contains(github.head_ref, 'bump/v') && github.event.pull_request.merged == true 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.merge_commit_sha }} 23 | fetch-depth: '0' 24 | 25 | - name: Import GPG signing key 26 | uses: crazy-max/ghaction-import-gpg@v6 27 | with: 28 | gpg_private_key: ${{ secrets.MAVEN_SIGNING_KEY }} 29 | passphrase: ${{ secrets.MAVEN_SIGNING_KEY_PASSPHRASE }} 30 | 31 | - name: Install JDK 32 | uses: actions/setup-java@v4 33 | with: 34 | distribution: 'temurin' 35 | java-version: '17' 36 | cache: 'maven' 37 | server-id: ossrh 38 | server-username: MVN_CENTRAL_USERNAME 39 | server-password: MVN_CENTRAL_PASSWORD 40 | 41 | - name: Release to Maven repo 42 | run: mvn -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" -Dgpg.passphrase="${{ secrets.MAVEN_SIGNING_KEY_PASSPHRASE }}" -Drevision="${{ env.TAG_NAME }}" -DnvdApiKey="${{ secrets.NVD_API_KEY }}" deploy 43 | env: 44 | MVN_CENTRAL_USERNAME: ${{ secrets.MVN_CENTRAL_USERNAME }} 45 | MVN_CENTRAL_PASSWORD: ${{ secrets.MVN_CENTRAL_PASSWORD }} 46 | 47 | - name: Push tag 48 | uses: anothrNick/github-tag-action@1.64.0 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | CUSTOM_TAG: ${{ env.TAG_NAME }} 52 | 53 | - name: Release 54 | uses: softprops/action-gh-release@v1 55 | with: 56 | tag_name: ${{ env.TAG_NAME }} 57 | generate_release_notes: true 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17.0 2 | -------------------------------------------------------------------------------- /.jpb/jpb-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if(mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if(mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if(!outputFile.getParentFile().exists()) { 87 | if(!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/.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.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | 6 | ## [0.2.10] - 14 Mar 2024 7 | ### Added 8 | - Add IsEmpty strategy 9 | - Add Between strategy 10 | - Add In strategy 11 | 12 | ## [0.2.6] - 10 Jan 2024 13 | ### Added 14 | - Bump Java, Kotlin and Spring versions 15 | - Bump various dependencies versions to avoid vulnerabilities 16 | - Various refactorings to leverage Java 17 features 17 | - Update README.md 18 | 19 | ## [0.2.5] - 09 Jan 2024 20 | ### Added 21 | - Add Instant strategy 22 | 23 | ## [0.2.4] - 12 Mar 2021 24 | ### Added 25 | - Add UUID strategy 26 | 27 | ## [0.2.3] - 12 Mar 2021 28 | ### Added 29 | - Add Enum strategy 30 | - Create Changelog 31 | 32 | ## [0.2.2] - 3 Mar 2021 33 | ### Added 34 | - Add case-sensitive flag for strings 35 | - Add tests to reach coverage threshold 36 | 37 | ### Changed 38 | - Update Readme.md 39 | - Refactor parsingStrategies to have a file per Strategy 40 | - Format files 41 | 42 | ## [0.1.1] - 4 Sep 2019 43 | ### Added 44 | - Add release process. 45 | 46 | ## [0.1.0] - 4 Sep 2019 47 | ### Added 48 | - First commit with annotation search parameters. 49 | 50 | [Unreleased]: https://github.com/sipios/spring-search/compare/spring-search-0.2.4...HEAD 51 | [0.2.4]: https://github.com/sipios/spring-search/compare/spring-search-0.2.3...spring-search-0.2.4 52 | [0.2.3]: https://github.com/sipios/spring-search/compare/spring-search-0.2.2...spring-search-0.2.3 53 | [0.2.2]: https://github.com/sipios/spring-search/compare/spring-search-0.1.1...spring-search-0.2.2 54 | [0.1.1]: https://github.com/sipios/spring-search/compare/spring-search-0.1.0...spring-search-0.1.1 55 | [0.1.0]: https://github.com/sipios/spring-search/releases/tag/spring-search-0.1.0 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | dev@sipios.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Ideas and pull requests are always welcome! 4 | 5 | ## Reporting Issues and suggesting features 6 | If you have found what you think is a bug or want to suggest a feature, please fill an issue using the appropriate template. 7 | 8 | ## Fixing bugs or implementing new features 9 | - Fork the repository 10 | - Create a new branch from `master` (`git checkout -b feat/AmazingFeature`) 11 | - Build the project (`mvn clean install`) 12 | - Make sure you have a test for your new feature or bugfix. 13 | Code coverage should be >= 75% and all tests should pass. You can run the tests with `mvn test` 14 | - Commit your changes following our [commit message convention](#Commit-messages) 15 | - Document your changes in the README.md if needed 16 | - Submit a pull request for review 17 | 18 | ## Commit messages 19 | This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). Please follow the convention when writing commit messages. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Woody Rousseau and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bump_project_version: 2 | ./scripts/bump_project_version.sh $(NEW_VERSION) 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | [![Contributors][contributors-shield]][contributors-url] 10 | [![Forks][forks-shield]][forks-url] 11 | [![Stargazers][stars-shield]][stars-url] 12 | [![Issues][issues-shield]][issues-url] 13 | [![MIT License][license-shield]][license-url] 14 | 15 | [![codecov][coverage-shield]][coverage-url] 16 | 17 | 18 |
19 |

20 | 21 | Logo 22 | 23 | 24 |

Spring Search

25 | 26 |

27 | Spring Search provides advanced search capabilities to a JPA entity 28 |
29 | Explore the docs » 30 |
31 |
32 | View Demo 33 | · 34 | Report Bug 35 | · 36 | Request Feature 37 |

38 |

39 | 40 | 41 | 42 | 43 | ## Table of Contents 44 | 45 | * [About the Project](#about-the-project) 46 | * [Built With](#built-with) 47 | * [Getting Started](#getting-started) 48 | * [Prerequisites](#prerequisites) 49 | * [Installation](#installation) 50 | * [Usage](#usage) 51 | * [Roadmap](#roadmap) 52 | * [Contributing](#contributing) 53 | * [License](#license) 54 | * [Contact](#contact) 55 | * [Acknowledgements](#acknowledgements) 56 | 57 | 58 | 59 | 60 | ## About The Project 61 | 62 | [![Spring-search screenshot][product-screenshot]](./docs/images/complex-example) 63 | 64 | Spring Search provides a simple query language to perform advanced searches for your JPA entities. 65 | 66 | Let's say you manage cars, and you want to allow API consumers to search for: 67 | * Cars that are blue **and** that were created after year 2006 **or** whose model name contains "Vanquish" 68 | * Cars whose brand is "Aston Martin" **or** whose price is more than 10000$ 69 | 70 | You could either create custom repository methods for both these operations, which works well if you know in advance which fields users might want to perform searches on. You could also use spring-search that allows searching on all fields, combining logical operators and much more. 71 | 72 | Please note that providing such a feature on your API does not come without risks such as performance issues and less clear capabilities for your API. [This article](http://www.bizcoder.com/don-t-design-a-query-string-you-will-one-day-regret) summarizes these risks well. 73 | 74 | ### Built With 75 | 76 | * [Kotlin](https://kotlinlang.org/) 77 | * [Spring Boot](https://spring.io/projects/spring-boot) 78 | * [ANTLR](https://www.antlr.org/) 79 | 80 | 81 | ## Getting Started 82 | 83 | **Requirements** : JDK 8 or more. 84 | To get a local copy up and running follow these simple steps. 85 | 86 | ### Installation 87 | ##### Maven 88 | 89 | Add the repo to your project inside your `pom.xml` file 90 | ```xml 91 | 92 | com.sipios 93 | spring-search 94 | 0.2.6 95 | 96 | ``` 97 | 98 | ##### Gradle 99 | Add the repo to your project by adding `implementation 'com.sipios:spring-search:0.2.0'` in your `build.gradle` file. 100 | 101 | 102 | ## Usage 103 | 104 | Your repository should be annotated as a `RepositoryRestResource` and should extend `JpaSpecificationExecutor` 105 | ```kotlin 106 | import org.springframework.data.jpa.repository.JpaRepository 107 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor 108 | import org.springframework.data.rest.core.annotation.RepositoryRestResource 109 | 110 | @RepositoryRestResource 111 | interface YourRepository : JpaRepository, JpaSpecificationExecutor 112 | ``` 113 | 114 | Import the library in your controller 115 | ```kotlin 116 | import com.sipios.springsearch.anotation.SearchSpec 117 | ``` 118 | 119 | Use it in your controller 120 | ```kotlin 121 | @GetMapping("searchUrl") 122 | fun yourFunctionNameHere(@SearchSpec specs: Specification): ResponseEntity> { 123 | return ResponseEntity(yourRepository.findAll(Specification.where(specs)), HttpStatus.OK) 124 | } 125 | ``` 126 | 127 | ## Operators 128 | 129 | | Operator | Description | Example | 130 | |----------------|---------------------------------|----------------------------------------------------| 131 | | `:` | Equal | `color:Red` | 132 | | `!` | Not equal | `color!Red` | 133 | | `>` | Greater than | `creationyear>2017` | 134 | | `>:` | Greater than eq | `creationyear>2017` | 135 | | `<` | Less than | `price<100000` | 136 | | `<:` | Less than eq | `price<100000` | 137 | | `*` | Starts with | `brand:*Martin` | 138 | | `*` | Ends with | `brand:Aston*` | 139 | | `*` | Contains | `brand:*Martin*` | 140 | | `OR` | Logical OR | `color:Red OR color:Blue` | 141 | | `AND` | Logical AND | `brand:Aston* AND price<300000` | 142 | | `IN` | Value is in list | `color IN ['Red', 'Blue']` | 143 | | `NOT IN` | Value is not in list | `color NOT IN ['Red', 'Blue']` | 144 | | `IS EMPTY` | Collection field is empty | `cars IS EMPTY` | 145 | | `IS NOT EMPTY` | Collection field is not empty | `cars IS NOT EMPTY` | 146 | | `IS NULL` | Field is null | `brand IS NULL` | 147 | | `IS NOT NULL` | Field is not null | `brand IS NOT NULL` | 148 | | `()` | Parenthesis | `brand:Nissan OR (brand:Chevrolet AND color:Blue)` | 149 | | `BETWEEN` | Value is between two values | `creationyear BETWEEN 2017 AND 2019` | 150 | | `NOT BETWEEN` | Value is not between two values | `creationyear NOT BETWEEN 2017 AND 2019` | 151 | 152 | 153 | ## Examples 154 | 155 | 1. Using parenthesis 156 | Request : `/cars?search=( brand:Nissan OR brand:Chevrolet ) AND color:Blue` 157 | *Note: Spaces inside the parenthesis are not necessary* 158 | ![parenthesis example](./docs/images/parenthesis-example.gif) 159 | 2. Using space in nouns 160 | Request : `/cars?search=model:'Spacetourer Business Lounge'` 161 | ![space example](./docs/images/space-example.gif) 162 | 3. Using special characters 163 | Request: `/cars?search=model:中华V7` 164 | ![special characters example](./docs/images/special-characters-example.gif) 165 | 4. Using deep fields 166 | Request : `/cars?search=options.transmission:Auto` 167 | ![deep field example](./docs/images/deep-field-example.gif) 168 | 5. Complex example 169 | Request : `/cars?search=creationyear:2018 AND price<300000 AND (color:Yellow OR color:Blue) AND options.transmission:Auto` 170 | ![complex example](./docs/images/complex-example.gif) 171 | 15. Using the BETWEEN operator 172 | Request : `/cars?search=creationyear BETWEEN 2017 AND 2019` 173 | 174 | ## Blocking the search on a field 175 | ```java 176 | @GetMapping 177 | public List getUsers(@SearchSpec(blackListedFields = {"password"}) Specification specs) { 178 | return userRepository.findAll(Specification.where(specs)); 179 | } 180 | ``` 181 | 182 | 183 | ## Troubleshooting 184 | 185 | If you get the following error ⬇️ 186 | 187 | > No primary or default constructor found for interface org.springframework.data.jpa.domain.Specification 188 | 189 | You are free to opt for either of the two following solutions : 190 | 1. Add a `@Configuration` class to add our argument resolver to your project 191 | ```kotlin 192 | // Kotlin 193 | @Configuration 194 | class SpringSearchResolverConf : WebMvcConfigurer { 195 | override fun addArgumentResolvers(argumentResolvers: MutableList) { 196 | argumentResolvers.add(SearchSpecificationResolver()) 197 | } 198 | } 199 | ``` 200 | ```java 201 | // Java 202 | @Configuration 203 | public class SpringSearchResolverConf implements WebMvcConfigurer { 204 | @Override 205 | public void addArgumentResolvers(List argumentResolvers) { 206 | argumentResolvers.add(new SearchSpecificationResolver()); 207 | } 208 | } 209 | ``` 210 | 211 | 2. Add a `@ComponentScan` annotation to your project configuration class 212 | ```java 213 | @ComponentScan(basePackages = {"com.your-application", "com.sipios.springsearch"}) 214 | ``` 215 | 216 | 217 | ## Roadmap 218 | 219 | See the [open issues](https://github.com/sipios/spring-search/issues) for a list of proposed features (and known issues). 220 | 221 | Please note that boolean parameter types are yet to be supported. 222 | 223 | 224 | 225 | ## Contributing 226 | 227 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 228 | 229 | 1. Fork the Project 230 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 231 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 232 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 233 | 5. Open a Pull Request 234 | 235 | 236 | 237 | 238 | ## License 239 | 240 | Distributed under the MIT License. See `LICENSE` for more information. 241 | 242 | 243 | 244 | 245 | ## Contact 246 | 247 | [@sipios_fintech](https://twitter.com/sipios_fintech) - contact@sipios.com 248 | 249 | Project Link: [https://github.com/sipios/spring-search](https://github.com/sipios/spring-search) 250 | 251 | 252 | 253 | 254 | [contributors-shield]: https://img.shields.io/github/contributors/sipios/spring-search.svg?style=flat-square 255 | [contributors-url]: https://github.com/sipios/spring-search/graphs/contributors 256 | [forks-shield]: https://img.shields.io/github/forks/sipios/spring-search.svg?style=flat-square 257 | [forks-url]: https://github.com/sipios/spring-search/network/members 258 | [stars-shield]: https://img.shields.io/github/stars/sipios/spring-search.svg?style=flat-square 259 | [stars-url]: https://github.com/sipios/spring-search/stargazers 260 | [issues-shield]: https://img.shields.io/github/issues/sipios/spring-search.svg?style=flat-square 261 | [issues-url]: https://github.com/sipios/spring-search/issues 262 | [license-shield]: https://img.shields.io/github/license/sipios/spring-search.svg?style=flat-square 263 | [license-url]: https://github.com/sipios/spring-search/blob/master/LICENSE.txt 264 | [product-screenshot]: docs/images/complex-example.gif 265 | [coverage-shield]: https://codecov.io/gh/sipios/spring-search/branch/master/graph/badge.svg 266 | [coverage-url]: https://codecov.io/gh/sipios/spring-search 267 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: spring-search 5 | annotations: 6 | github.com/project-slug: sipios/spring-search 7 | spec: 8 | type: other 9 | lifecycle: unknown 10 | owner: user:luc-boussant 11 | -------------------------------------------------------------------------------- /docs/images/and-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/and-example.gif -------------------------------------------------------------------------------- /docs/images/complex-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/complex-example.gif -------------------------------------------------------------------------------- /docs/images/deep-field-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/deep-field-example.gif -------------------------------------------------------------------------------- /docs/images/equal-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/equal-example.gif -------------------------------------------------------------------------------- /docs/images/greater-than-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/greater-than-example.gif -------------------------------------------------------------------------------- /docs/images/less-than-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/less-than-example.gif -------------------------------------------------------------------------------- /docs/images/not-equal-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/not-equal-example.gif -------------------------------------------------------------------------------- /docs/images/or-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/or-example.gif -------------------------------------------------------------------------------- /docs/images/parenthesis-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/parenthesis-example.gif -------------------------------------------------------------------------------- /docs/images/space-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/space-example.gif -------------------------------------------------------------------------------- /docs/images/special-characters-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/special-characters-example.gif -------------------------------------------------------------------------------- /docs/images/starts-with-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/docs/images/starts-with-example.gif -------------------------------------------------------------------------------- /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 | # Maven2 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 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /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 Maven2 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 key stroke 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 my 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.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 3.1.7 8 | 9 | 10 | com.sipios 11 | spring-search 12 | 0.3.0 13 | spring-search 14 | API for generating spring boot database queries 15 | 16 | 17 | 17 18 | 1.9.22 19 | 0.8.12 20 | 2.2.220 21 | 2.16.0 22 | 2.0 23 | 9.0.7 24 | 4.13.1 25 | com.sipios.springsearch.grammar 26 | com/sipios/springsearch/grammar 27 | ${project.build.directory}/test-results 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jpa 34 | provided 35 | 36 | 37 | com.h2database 38 | h2 39 | ${h2.version} 40 | test 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-data-rest 45 | provided 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-web 50 | provided 51 | 52 | 53 | com.fasterxml.jackson.core 54 | jackson-databind 55 | ${jackson-databind.version} 56 | provided 57 | 58 | 59 | org.yaml 60 | snakeyaml 61 | ${snakeyaml.version} 62 | provided 63 | 64 | 65 | com.fasterxml.jackson.module 66 | jackson-module-kotlin 67 | 68 | 69 | org.jetbrains.kotlin 70 | kotlin-reflect 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-test 76 | test 77 | 78 | 79 | 80 | org.antlr 81 | antlr4-runtime 82 | ${antl4.version} 83 | 84 | 85 | org.jetbrains.kotlin 86 | kotlin-stdlib-jdk8 87 | ${kotlin.version} 88 | 89 | 90 | org.jetbrains.kotlin 91 | kotlin-test 92 | ${kotlin.version} 93 | test 94 | 95 | 96 | 97 | 98 | ${project.basedir}/src/main/kotlin 99 | src/test/kotlin 100 | 101 | 102 | org.antlr 103 | antlr4-maven-plugin 104 | ${antl4.version} 105 | 106 | 107 | antlr 108 | 109 | antlr4 110 | 111 | 112 | 113 | 114 | true 115 | true 116 | 117 | -package 118 | ${grammar.package} 119 | 120 | ${project.build.directory}/generated-sources/antlr4/${grammar.directory} 121 | 122 | 123 | 124 | org.jetbrains.kotlin 125 | kotlin-maven-plugin 126 | ${kotlin.version} 127 | 128 | 129 | compile 130 | process-sources 131 | 132 | compile 133 | 134 | 135 | 136 | src/main/kotlin 137 | target/generated-sources/antlr4 138 | 139 | 140 | 141 | 142 | test-compile 143 | test-compile 144 | 145 | test-compile 146 | 147 | 148 | 149 | src/main/kotlin 150 | src/test/kotlin 151 | target/generated-sources/antlr4 152 | 153 | 154 | 155 | 156 | 157 | 158 | -Xjsr305=strict 159 | 160 | 161 | spring 162 | jpa 163 | 164 | 165 | src/main/kotlin 166 | target/generated-sources/antlr4 167 | 168 | 17 169 | 170 | 171 | 172 | org.jetbrains.kotlin 173 | kotlin-maven-allopen 174 | ${kotlin.version} 175 | 176 | 177 | org.jetbrains.kotlin 178 | kotlin-maven-noarg 179 | ${kotlin.version} 180 | 181 | 182 | 183 | 184 | maven-release-plugin 185 | 2.5.3 186 | 187 | false 188 | release 189 | true 190 | 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-antrun-plugin 195 | 1.8 196 | 197 | 198 | ktlint 199 | compile 200 | 201 | 202 | 204 | 205 | 206 | 207 | 208 | run 209 | 210 | 211 | ktlint-format 212 | 213 | 214 | 216 | 217 | 218 | 219 | 220 | 221 | run 222 | 223 | 224 | 225 | 226 | com.pinterest 227 | ktlint 228 | 0.34.2 229 | 230 | 231 | 232 | 233 | 234 | org.jacoco 235 | jacoco-maven-plugin 236 | ${jacoco-maven-plugin.version} 237 | 238 | 239 | ${grammar.directory}/**/* 240 | com/sipios/springsearch/configuration/**/* 241 | 242 | 243 | 244 | 245 | pre-unit-tests 246 | 247 | prepare-agent 248 | 249 | 250 | 251 | ${project.testresult.directory}/coverage/jacoco/jacoco.exec 252 | 253 | 254 | 255 | 256 | post-unit-test 257 | test 258 | 259 | report 260 | 261 | 262 | ${project.testresult.directory}/coverage/jacoco/jacoco.exec 263 | ${project.testresult.directory}/coverage/jacoco 264 | 265 | 266 | 267 | jacoco-check 268 | test 269 | 270 | check 271 | 272 | 273 | ${project.testresult.directory}/coverage/jacoco/jacoco.exec 274 | 275 | 276 | BUNDLE 277 | 278 | 279 | INSTRUCTION 280 | COVEREDRATIO 281 | 0.80 282 | 283 | 284 | BRANCH 285 | COVEREDRATIO 286 | 0.75 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | org.apache.maven.plugins 297 | maven-compiler-plugin 298 | 3.8.1 299 | 300 | 301 | compile 302 | compile 303 | 304 | compile 305 | 306 | 307 | 308 | testCompile 309 | test-compile 310 | 311 | testCompile 312 | 313 | 314 | 315 | 316 | 317 | maven-source-plugin 318 | 319 | 320 | attach-sources 321 | package 322 | 323 | jar-no-fork 324 | 325 | 326 | 327 | 328 | 329 | maven-javadoc-plugin 330 | 331 | 332 | attach-javadocs 333 | package 334 | 335 | jar 336 | 337 | 338 | 339 | 340 | false 341 | 342 | 343 | 344 | 345 | maven-deploy-plugin 346 | 347 | 348 | deploy 349 | deploy 350 | 351 | deploy 352 | 353 | 354 | 355 | 356 | 357 | org.apache.maven.plugins 358 | maven-gpg-plugin 359 | 3.1.0 360 | 361 | 362 | sign-artifacts 363 | verify 364 | 365 | sign 366 | 367 | 368 | 369 | --pinentry-mode 370 | loopback 371 | 372 | 373 | 374 | 375 | 376 | 377 | org.owasp 378 | dependency-check-maven 379 | ${dependency-check-maven.version} 380 | 381 | 382 | 383 | check 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | scm:git:https://github.com/sipios/spring-search.git 392 | scm:git:git@github.com:sipios/spring-search.git 393 | https://github.com/sipios/spring-search 394 | HEAD 395 | 396 | 397 | 398 | 399 | ossrh 400 | https://oss.sonatype.org/service/local/staging/deploy/maven2 401 | 402 | 403 | 404 | -------------------------------------------------------------------------------- /scripts/bump_project_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if new_version argument is provided 4 | if [ $# -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # Assign the new_version argument to a variable 10 | new_version="$1" 11 | commit_message="chore: bump project version to $new_version" 12 | branch_name="bump/v$new_version" 13 | 14 | # Checkout master branch and pull 15 | git checkout master 16 | git pull 17 | 18 | # Bump project version 19 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion="$new_version" 20 | 21 | # Commit & push changes 22 | git checkout -b "$branch_name" 23 | git add pom.xml 24 | git commit -m "$commit_message" 25 | git push origin "$branch_name" 26 | 27 | # Create pull request 28 | gh pr create --title "spring-search-$new_version" --body "Automated PR for project version bump" --base master --head "$branch_name" 29 | -------------------------------------------------------------------------------- /src/main/antlr4/Query.g4: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019, Michael Mollard 3 | */ 4 | 5 | grammar Query; 6 | 7 | // syntactic rules 8 | input 9 | : query EOF 10 | ; 11 | 12 | query 13 | : left=query logicalOp=(AND | OR) right=query #opQuery 14 | | LPAREN query RPAREN #priorityQuery 15 | | criteria #atomQuery 16 | ; 17 | 18 | criteria 19 | : key (IN | NOT_IN) array #eqArrayCriteria 20 | | key op value #opCriteria 21 | | key (BETWEEN | NOT_BETWEEN) left=value AND right=value #betweenCriteria 22 | | key (IS | IS_NOT) is_value #isCriteria 23 | ; 24 | 25 | is_value 26 | : EMPTY 27 | | NULL 28 | ; 29 | 30 | key 31 | : IDENTIFIER 32 | ; 33 | 34 | array 35 | : LBRACKET (value (',' value)* )? RBRACKET 36 | ; 37 | 38 | value 39 | : IDENTIFIER 40 | | STRING 41 | | ENCODED_STRING 42 | | NUMBER 43 | | BOOL 44 | ; 45 | 46 | op 47 | : EQ 48 | | GT 49 | | GTE 50 | | LT 51 | | LTE 52 | | NOT_EQ 53 | ; 54 | 55 | // lexical rules 56 | BOOL 57 | : 'true' 58 | | 'false' 59 | ; 60 | 61 | NULL 62 | : 'NULL' 63 | ; 64 | 65 | STRING 66 | : '"' DoubleStringCharacter* '"' 67 | | '\'' SingleStringCharacter* '\'' 68 | ; 69 | 70 | fragment DoubleStringCharacter 71 | : ~["\\\r\n] 72 | | '\\' EscapeSequence 73 | | LineContinuation 74 | ; 75 | 76 | fragment SingleStringCharacter 77 | : ~['\\\r\n] 78 | | '\\' EscapeSequence 79 | | LineContinuation 80 | ; 81 | 82 | fragment EscapeSequence 83 | : CharacterEscapeSequence 84 | | HexEscapeSequence 85 | | UnicodeEscapeSequence 86 | ; 87 | 88 | fragment CharacterEscapeSequence 89 | : SingleEscapeCharacter 90 | | NonEscapeCharacter 91 | ; 92 | 93 | fragment HexEscapeSequence 94 | : 'x' HexDigit HexDigit 95 | ; 96 | 97 | fragment UnicodeEscapeSequence 98 | : 'u' HexDigit HexDigit HexDigit HexDigit 99 | ; 100 | 101 | fragment SingleEscapeCharacter 102 | : ['"\\bfnrtv] 103 | ; 104 | 105 | fragment NonEscapeCharacter 106 | : ~['"\\bfnrtv0-9xu\r\n] 107 | ; 108 | 109 | fragment EscapeCharacter 110 | : SingleEscapeCharacter 111 | | DecimalDigit 112 | | [xu] 113 | ; 114 | 115 | fragment LineContinuation 116 | : '\\' LineTerminatorSequence 117 | ; 118 | 119 | fragment LineTerminatorSequence 120 | : '\r\n' 121 | | LineTerminator 122 | ; 123 | 124 | fragment DecimalDigit 125 | : [0-9] 126 | ; 127 | fragment HexDigit 128 | : [0-9a-fA-F] 129 | ; 130 | fragment OctalDigit 131 | : [0-7] 132 | ; 133 | 134 | AND 135 | : 'AND' 136 | ; 137 | 138 | OR 139 | : 'OR' 140 | ; 141 | 142 | NUMBER 143 | : ('0' .. '9') ('0' .. '9')* POINT? ('0' .. '9')* 144 | ; 145 | 146 | LPAREN 147 | : '(' 148 | ; 149 | 150 | 151 | RPAREN 152 | : ')' 153 | ; 154 | 155 | LBRACKET 156 | : '[' 157 | ; 158 | 159 | RBRACKET 160 | : ']' 161 | ; 162 | 163 | GT 164 | : '>' 165 | ; 166 | 167 | GTE 168 | : '>:' 169 | ; 170 | 171 | LT 172 | : '<' 173 | ; 174 | 175 | LTE 176 | : '<:' 177 | ; 178 | 179 | EQ 180 | : ':' 181 | ; 182 | 183 | IS 184 | : 'IS' 185 | ; 186 | 187 | IS_NOT 188 | : 'IS NOT' 189 | ; 190 | 191 | EMPTY 192 | : 'EMPTY' 193 | ; 194 | 195 | NOT_EQ 196 | : '!' 197 | ; 198 | 199 | BETWEEN 200 | : 'BETWEEN' 201 | ; 202 | 203 | NOT_BETWEEN 204 | : 'NOT BETWEEN' 205 | ; 206 | IN 207 | : 'IN' 208 | ; 209 | 210 | NOT_IN 211 | : 'NOT IN' 212 | ; 213 | 214 | fragment POINT 215 | : '.' 216 | ; 217 | 218 | IDENTIFIER 219 | : [A-Za-z0-9.]+ 220 | ; 221 | 222 | ENCODED_STRING //anything but these characters :<>!()[], and whitespace 223 | : ~([ ,:<>!()[\]])+ 224 | ; 225 | 226 | 227 | LineTerminator 228 | : [\r\n\u2028\u2029] -> channel(HIDDEN) 229 | ; 230 | 231 | WS 232 | : [ \t\r\n]+ -> skip 233 | ; -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/CriteriaParser.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import SyntaxErrorListener 4 | import com.sipios.springsearch.anotation.SearchSpec 5 | import com.sipios.springsearch.grammar.QueryLexer 6 | import com.sipios.springsearch.grammar.QueryParser 7 | import org.antlr.v4.runtime.CharStreams 8 | import org.antlr.v4.runtime.CommonTokenStream 9 | import org.springframework.data.jpa.domain.Specification 10 | import org.springframework.http.HttpStatus 11 | import org.springframework.web.server.ResponseStatusException 12 | 13 | /** 14 | * Class used to parse a search query string and create a specification 15 | */ 16 | class CriteriaParser(searchSpecAnnotation: SearchSpec) { 17 | 18 | private val visitor = QueryVisitorImpl(searchSpecAnnotation) 19 | 20 | /** 21 | * Lexer -> Parser -> Visitor are used to build the specification 22 | * 23 | * @param searchParam The search param 24 | * @return a specification matching the input 25 | */ 26 | fun parse(searchParam: String): Specification { 27 | val parser = getParser(searchParam) 28 | val listener = SyntaxErrorListener() 29 | parser.addErrorListener(listener) 30 | // complete parse tree before visiting 31 | val input = parser.input() 32 | if (parser.numberOfSyntaxErrors > 0) { 33 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid search query: $listener") 34 | } 35 | return visitor.visit(input) 36 | } 37 | 38 | private fun getParser(queryString: String): QueryParser { 39 | val lexer = QueryLexer(CharStreams.fromString(queryString)) 40 | val tokens = CommonTokenStream(lexer) 41 | return QueryParser(tokens) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import com.sipios.springsearch.anotation.SearchSpec 4 | import com.sipios.springsearch.grammar.QueryBaseVisitor 5 | import com.sipios.springsearch.grammar.QueryParser 6 | import org.springframework.data.jpa.domain.Specification 7 | import org.springframework.http.HttpStatus 8 | import org.springframework.web.server.ResponseStatusException 9 | 10 | class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryBaseVisitor>() { 11 | private val valueRegExp = Regex(pattern = "^(?\\*?)(?.+?)(?\\*?)$") 12 | override fun visitOpQuery(ctx: QueryParser.OpQueryContext): Specification { 13 | val left = visit(ctx.left) 14 | val right = visit(ctx.right) 15 | 16 | return when (ctx.logicalOp.text) { 17 | "AND" -> left.and(right) 18 | "OR" -> left.or(right) 19 | else -> left.and(right) 20 | } 21 | } 22 | 23 | override fun visitPriorityQuery(ctx: QueryParser.PriorityQueryContext): Specification { 24 | return visit(ctx.query()) 25 | } 26 | 27 | override fun visitAtomQuery(ctx: QueryParser.AtomQueryContext): Specification { 28 | return visit(ctx.criteria()) 29 | } 30 | 31 | override fun visitInput(ctx: QueryParser.InputContext): Specification { 32 | return visit(ctx.query()) 33 | } 34 | 35 | override fun visitIsCriteria(ctx: QueryParser.IsCriteriaContext): Specification { 36 | val key = ctx.key().text 37 | verifyBlackList(key) 38 | val op = if (ctx.IS() != null) { 39 | SearchOperation.IS 40 | } else { 41 | SearchOperation.IS_NOT 42 | } 43 | return toSpec(key, op, ctx.is_value().text) 44 | } 45 | 46 | override fun visitEqArrayCriteria(ctx: QueryParser.EqArrayCriteriaContext): Specification { 47 | val key = ctx.key().text 48 | verifyBlackList(key) 49 | val op = if (ctx.IN() != null) { 50 | SearchOperation.IN_ARRAY 51 | } else { 52 | SearchOperation.NOT_IN_ARRAY 53 | } 54 | val arr = ctx.array() 55 | val arrayValues = arr.value() 56 | val valueAsList: List = 57 | arrayValues.map { if (it.STRING() != null) clearString(it.text) else it.text } 58 | // there is no need for prefix and suffix (e.g. 'john*') in case of array value 59 | val criteria = SearchCriteria(key, op, valueAsList) 60 | return SpecificationImpl(criteria, searchSpecAnnotation) 61 | } 62 | 63 | override fun visitBetweenCriteria(ctx: QueryParser.BetweenCriteriaContext): Specification { 64 | val key = ctx.key().text 65 | verifyBlackList(key) 66 | val leftValue = if (ctx.left.STRING() != null) { 67 | clearString(ctx.left.text) 68 | } else { 69 | ctx.left.text 70 | } 71 | val rightValue = if (ctx.right.STRING() != null) { 72 | clearString(ctx.right.text) 73 | } else { 74 | ctx.right.text 75 | } 76 | val leftExp = toSpec(key, SearchOperation.GREATER_THAN_EQUALS, leftValue) 77 | val rightExp = toSpec(key, SearchOperation.LESS_THAN_EQUALS, rightValue) 78 | return if (ctx.BETWEEN() != null) { 79 | leftExp.and(rightExp) 80 | } else { 81 | Specification.not(leftExp.and(rightExp)) 82 | } 83 | } 84 | 85 | private fun toSpec( 86 | key: String, 87 | opLeft: SearchOperation, 88 | leftValue: String? 89 | ): SpecificationImpl { 90 | val criteria = SearchCriteria(key, opLeft, leftValue) 91 | return SpecificationImpl(criteria, searchSpecAnnotation) 92 | } 93 | 94 | override fun visitOpCriteria(ctx: QueryParser.OpCriteriaContext): Specification { 95 | val key = ctx.key().text 96 | val value = if (ctx.value().STRING() != null) { 97 | clearString(ctx.value().text) 98 | } else { 99 | ctx.value().text 100 | } 101 | verifyBlackList(key) 102 | val matchResult = this.valueRegExp.find(value) 103 | val op = SearchOperation.getSimpleOperation(ctx.op().text) ?: throw IllegalArgumentException("Invalid operation") 104 | val criteria = SearchCriteria( 105 | key, 106 | op, 107 | matchResult!!.groups["prefix"]!!.value, 108 | matchResult.groups["value"]!!.value, 109 | matchResult.groups["suffix"]!!.value 110 | ) 111 | 112 | return SpecificationImpl(criteria, searchSpecAnnotation) 113 | } 114 | 115 | private fun verifyBlackList(key: String?) { 116 | val blackList = this.searchSpecAnnotation.blackListedFields 117 | if (blackList.contains(key)) { 118 | throw ResponseStatusException( 119 | HttpStatus.BAD_REQUEST, 120 | "Field $key is blacklisted" 121 | ) 122 | } 123 | } 124 | 125 | private fun clearString(value: String) = value 126 | .removeSurrounding("'") 127 | .removeSurrounding("\"") 128 | .replace("\\\"", "\"") 129 | .replace("\\'", "'") 130 | } 131 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | class SearchCriteria // Change EQUALS into ENDS_WITH, CONTAINS, STARTS_WITH based on the presence of * in the value 4 | (var key: String, private var op: SearchOperation, prefix: String?, val value: Any?, suffix: String?) { 5 | var operation: SearchOperation? 6 | 7 | init { 8 | // Change EQUALS into ENDS_WITH, CONTAINS, STARTS_WITH based on the presence of * in the value 9 | val startsWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX) 10 | val endsWithAsterisk = suffix != null && suffix.contains(SearchOperation.ZERO_OR_MORE_REGEX) 11 | op = when { 12 | op === SearchOperation.EQUALS && startsWithAsterisk && endsWithAsterisk -> SearchOperation.CONTAINS 13 | op === SearchOperation.EQUALS && startsWithAsterisk -> SearchOperation.ENDS_WITH 14 | op === SearchOperation.EQUALS && endsWithAsterisk -> SearchOperation.STARTS_WITH 15 | else -> op 16 | } 17 | 18 | // Change NOT_EQUALS into DOESNT_END_WITH, DOESNT_CONTAIN, DOESNT_START_WITH based on the presence of * in the value 19 | op = when { 20 | op === SearchOperation.NOT_EQUALS && startsWithAsterisk && endsWithAsterisk -> SearchOperation.DOESNT_CONTAIN 21 | op === SearchOperation.NOT_EQUALS && startsWithAsterisk -> SearchOperation.DOESNT_END_WITH 22 | op === SearchOperation.NOT_EQUALS && endsWithAsterisk -> SearchOperation.DOESNT_START_WITH 23 | else -> op 24 | } 25 | this.operation = op 26 | } 27 | constructor(key: String, op: SearchOperation, value: Any?) : this(key, op, null, value, null) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/SearchOperation.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | enum class SearchOperation { 4 | EQUALS, 5 | NOT_EQUALS, 6 | GREATER_THAN, 7 | LESS_THAN, 8 | STARTS_WITH, 9 | ENDS_WITH, 10 | CONTAINS, 11 | DOESNT_START_WITH, 12 | DOESNT_END_WITH, 13 | DOESNT_CONTAIN, 14 | GREATER_THAN_EQUALS, 15 | LESS_THAN_EQUALS, 16 | IN_ARRAY, 17 | NOT_IN_ARRAY, 18 | IS, 19 | IS_NOT, 20 | BETWEEN, 21 | NOT_BETWEEN, 22 | ; 23 | 24 | companion object { 25 | val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~", ">:", "<:") 26 | val ZERO_OR_MORE_REGEX = "*" 27 | val OR_OPERATOR = "OR" 28 | val AND_OPERATOR = "AND" 29 | val LEFT_PARANTHESIS = "(" 30 | val RIGHT_PARANTHESIS = ")" 31 | val EMPTY = "EMPTY" 32 | val NULL = "NULL" 33 | 34 | /** 35 | * Parse a string into an operation. 36 | * 37 | * @param input operation as string 38 | * @return The matching operation 39 | */ 40 | fun getSimpleOperation( 41 | input: String 42 | ): SearchOperation? { 43 | return when (input) { 44 | ":" -> EQUALS 45 | "!" -> NOT_EQUALS 46 | ">" -> GREATER_THAN 47 | "<" -> LESS_THAN 48 | ">:" -> GREATER_THAN_EQUALS 49 | "<:" -> LESS_THAN_EQUALS 50 | else -> null 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/SpecificationImpl.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import com.sipios.springsearch.anotation.SearchSpec 4 | import com.sipios.springsearch.strategies.ParsingStrategy 5 | import jakarta.persistence.criteria.CriteriaBuilder 6 | import jakarta.persistence.criteria.CriteriaQuery 7 | import jakarta.persistence.criteria.Path 8 | import jakarta.persistence.criteria.Predicate 9 | import jakarta.persistence.criteria.Root 10 | import java.util.ArrayList 11 | import kotlin.reflect.KClass 12 | import org.springframework.data.jpa.domain.Specification 13 | import org.springframework.http.HttpStatus 14 | import org.springframework.web.server.ResponseStatusException 15 | 16 | /** 17 | * Implementation of the JPA Specification based on a Search Criteria 18 | * 19 | * @see Specification 20 | * 21 | * @param The class on which the specification will be applied 22 | * */ 23 | class SpecificationImpl(private val criteria: SearchCriteria, private val searchSpecAnnotation: SearchSpec) : 24 | Specification { 25 | @Throws(ResponseStatusException::class) 26 | override fun toPredicate( 27 | root: Root, 28 | query: CriteriaQuery<*>, 29 | builder: CriteriaBuilder 30 | ): Predicate? { 31 | val nestedKey = criteria.key.split(".") 32 | val nestedRoot = getNestedRoot(root, nestedKey) 33 | val criteriaKey = nestedKey[nestedKey.size - 1] 34 | val fieldClass = nestedRoot.get(criteriaKey).javaType.kotlin 35 | val isCollectionField = isCollectionType(nestedRoot.javaType, criteriaKey) 36 | val strategy = ParsingStrategy.getStrategy(fieldClass, searchSpecAnnotation, isCollectionField) 37 | val value = parseValue(strategy, fieldClass, criteriaKey, criteria.value) 38 | return strategy.buildPredicate(builder, nestedRoot, criteriaKey, criteria.operation, value) 39 | } 40 | 41 | private fun getNestedRoot( 42 | root: Root, 43 | nestedKey: List 44 | ): Path<*> { 45 | val prefix = ArrayList(nestedKey) 46 | prefix.removeAt(nestedKey.size - 1) 47 | var temp: Path<*> = root 48 | for (s in prefix) { 49 | temp = temp.get(s) 50 | } 51 | return temp 52 | } 53 | 54 | private fun isCollectionType(clazz: Class<*>, fieldName: String): Boolean { 55 | try { 56 | val field = clazz.getDeclaredField(fieldName) 57 | val type = field.type 58 | return Collection::class.java.isAssignableFrom(type) || type.isArray 59 | } catch (e: NoSuchFieldException) { 60 | return false 61 | } 62 | } 63 | 64 | private fun parseValue( 65 | strategy: ParsingStrategy, 66 | fieldClass: KClass, 67 | criteriaKey: String, 68 | value: Any? 69 | ): Any? { 70 | return try { 71 | if (value is List<*>) { 72 | strategy.parse(value, fieldClass) 73 | } else { 74 | strategy.parse(value?.toString(), fieldClass) 75 | } 76 | } catch (e: Exception) { 77 | throw ResponseStatusException( 78 | HttpStatus.BAD_REQUEST, 79 | "Could not parse input for the field $criteriaKey as a ${fieldClass.simpleName}" 80 | ) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/SpecificationsBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import com.sipios.springsearch.anotation.SearchSpec 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.CriteriaQuery 6 | import jakarta.persistence.criteria.Predicate 7 | import jakarta.persistence.criteria.Root 8 | import org.springframework.data.jpa.domain.Specification 9 | 10 | class SpecificationsBuilder(searchSpecAnnotation: SearchSpec) { 11 | 12 | private var specs: Specification = NullSpecification() 13 | private val parser: CriteriaParser = CriteriaParser(searchSpecAnnotation) 14 | 15 | fun withSearch(search: String): SpecificationsBuilder { 16 | specs = parser.parse(search) 17 | 18 | return this 19 | } 20 | 21 | /** 22 | * This function expect a search string to have been provided. 23 | * The search string has been transformed into an Expression Queue with the format: [OR, value>100, AND, value<1000, label:*MONO*] 24 | * 25 | * @return A list of specification used to filter the underlying object using JPA specifications 26 | */ 27 | fun build(): Specification { 28 | return specs 29 | } 30 | } 31 | 32 | class NullSpecification : Specification { 33 | override fun toPredicate(root: Root, query: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder): Predicate? { 34 | return null 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/anotation/SearchSpec.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.anotation 2 | 3 | /** 4 | * An annotation for mapping query search string into a Specification for a JPA Domain 5 | * 6 | * This annotation can be used on a parameter in a Spring MVC RestRepository Class 7 | */ 8 | @Target(AnnotationTarget.VALUE_PARAMETER) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | annotation class SearchSpec( 11 | /** 12 | * The name of the query param that will be transformed into a specification 13 | */ 14 | val searchParam: String = "search", 15 | 16 | /** 17 | * A flag to indicate if the search needs to be case-sensitive or not 18 | */ 19 | val caseSensitiveFlag: Boolean = true, 20 | 21 | /** 22 | * A list of fields that should be excluded from the search 23 | */ 24 | val blackListedFields: Array = [] 25 | ) 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/configuration/ResolverConf.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.configuration 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.web.method.support.HandlerMethodArgumentResolver 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 6 | 7 | @Configuration 8 | class ResolverConf : WebMvcConfigurer { 9 | 10 | /** 11 | * Register a SearchSpecificationResolver instance to the list of argument resolver used by Spring MVC 12 | * @param argumentResolvers The current list of argumentResolversUsed by Spring MVC 13 | */ 14 | override fun addArgumentResolvers(argumentResolvers: MutableList) { 15 | argumentResolvers.add(SearchSpecificationResolver()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/configuration/SearchSpecificationResolver.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.configuration 2 | 3 | import com.sipios.springsearch.SpecificationsBuilder 4 | import com.sipios.springsearch.anotation.SearchSpec 5 | import org.slf4j.LoggerFactory 6 | import org.springframework.core.MethodParameter 7 | import org.springframework.data.jpa.domain.Specification 8 | import org.springframework.lang.NonNull 9 | import org.springframework.web.bind.support.WebDataBinderFactory 10 | import org.springframework.web.context.request.NativeWebRequest 11 | import org.springframework.web.method.support.HandlerMethodArgumentResolver 12 | import org.springframework.web.method.support.ModelAndViewContainer 13 | 14 | class SearchSpecificationResolver : HandlerMethodArgumentResolver { 15 | /** 16 | * Check if the method parameter is a Specification with the @SearchSpec annotation 17 | * 18 | * @param parameter THe method parameter to handle 19 | * @return True if it is the case 20 | */ 21 | override fun supportsParameter(@NonNull parameter: MethodParameter): Boolean { 22 | return parameter.parameterType === Specification::class.java && parameter.hasParameterAnnotation(SearchSpec::class.java) 23 | } 24 | 25 | /** 26 | * Get the query parameter by the name defined in the [SearchSpec.searchParam] 27 | * Then use it to build the specification 28 | * 29 | * @param parameter THe method parameter to handle 30 | * @return True if it is the case 31 | */ 32 | @Throws(Exception::class) 33 | override fun resolveArgument( 34 | @NonNull parameter: MethodParameter, 35 | mavContainer: ModelAndViewContainer?, 36 | webRequest: NativeWebRequest, 37 | binderFactory: WebDataBinderFactory? 38 | ): Specification<*>? { 39 | val def = parameter.getParameterAnnotation(SearchSpec::class.java) 40 | 41 | return buildSpecification(parameter.genericParameterType.javaClass, webRequest.getParameter(def!!.searchParam), def) 42 | } 43 | 44 | private fun buildSpecification(specClass: Class, search: String?, searchSpecAnnotation: SearchSpec): Specification? { 45 | logger.debug("Building specification for class {}", specClass) 46 | logger.debug("Search value found is {}", search) 47 | if (search.isNullOrEmpty()) { 48 | return null 49 | } 50 | val specBuilder = SpecificationsBuilder(searchSpecAnnotation) 51 | 52 | return specBuilder.withSearch(search).build() 53 | } 54 | 55 | companion object { 56 | private val logger = LoggerFactory.getLogger(SearchSpecificationResolver::class.java) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/listeners/SyntaxErrorListener.kt: -------------------------------------------------------------------------------- 1 | import org.antlr.v4.runtime.BaseErrorListener 2 | import org.antlr.v4.runtime.RecognitionException 3 | import org.antlr.v4.runtime.Recognizer 4 | 5 | class SyntaxErrorListener internal constructor() : BaseErrorListener() { 6 | private val messages: MutableList = ArrayList() 7 | 8 | override fun syntaxError( 9 | recognizer: Recognizer<*, *>?, 10 | offendingSymbol: Any, 11 | line: Int, 12 | charPositionInLine: Int, 13 | msg: String, 14 | e: RecognitionException? 15 | ) { 16 | messages.add("line $line:$charPositionInLine $msg") 17 | } 18 | 19 | override fun toString(): String { 20 | return messages.toString() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import kotlin.reflect.KClass 5 | 6 | class BooleanStrategy : ParsingStrategy { 7 | override fun parse(value: String?, fieldClass: KClass): Any? { 8 | if (value == SearchOperation.NULL) return value 9 | return value?.toBoolean() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import org.springframework.http.HttpStatus 8 | import org.springframework.web.server.ResponseStatusException 9 | 10 | class CollectionStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | if (ops == SearchOperation.IS && value == SearchOperation.EMPTY) { 19 | return builder.isEmpty(path[fieldName]) 20 | } 21 | if (ops == SearchOperation.IS_NOT && value == SearchOperation.EMPTY) { 22 | return builder.isNotEmpty(path[fieldName]) 23 | } 24 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, 25 | "Unsupported operation $ops $value for collection field $fieldName, " + 26 | "only IS EMPTY and IS NOT EMPTY are supported" 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import com.sipios.springsearch.SearchOperation 5 | import jakarta.persistence.criteria.CriteriaBuilder 6 | import jakarta.persistence.criteria.Path 7 | import jakarta.persistence.criteria.Predicate 8 | import java.text.DateFormat 9 | import java.util.Date 10 | import kotlin.reflect.KClass 11 | 12 | class DateStrategy : ParsingStrategy { 13 | private val standardDateFormat: DateFormat = StdDateFormat() 14 | 15 | override fun buildPredicate( 16 | builder: CriteriaBuilder, 17 | path: Path<*>, 18 | fieldName: String, 19 | ops: SearchOperation?, 20 | value: Any? 21 | ): Predicate? { 22 | return when (ops) { 23 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Date) 24 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Date) 25 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Date) 26 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Date) 27 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 28 | } 29 | } 30 | 31 | override fun parse(value: String?, fieldClass: KClass): Any? { 32 | if (value == SearchOperation.NULL) return value 33 | return standardDateFormat.parse(value) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import kotlin.reflect.KClass 8 | 9 | class DoubleStrategy : ParsingStrategy { 10 | override fun buildPredicate( 11 | builder: CriteriaBuilder, 12 | path: Path<*>, 13 | fieldName: String, 14 | ops: SearchOperation?, 15 | value: Any? 16 | ): Predicate? { 17 | return when (ops) { 18 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Double) 19 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Double) 20 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Double) 21 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Double) 22 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 23 | } 24 | } 25 | 26 | override fun parse(value: String?, fieldClass: KClass): Any? { 27 | if (value == SearchOperation.NULL) return value 28 | return value?.toDouble() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import java.time.Duration 8 | import kotlin.reflect.KClass 9 | 10 | class DurationStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | return when (ops) { 19 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Duration) 20 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Duration) 21 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Duration) 22 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Duration) 23 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 24 | } 25 | } 26 | 27 | override fun parse(value: String?, fieldClass: KClass): Any? { 28 | if (value == SearchOperation.NULL) return value 29 | return Duration.parse(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import kotlin.reflect.KClass 5 | 6 | class EnumStrategy : ParsingStrategy { 7 | override fun parse(value: String?, fieldClass: KClass): Any? { 8 | if (value == SearchOperation.NULL) return value 9 | return Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import kotlin.reflect.KClass 8 | 9 | class FloatStrategy : ParsingStrategy { 10 | override fun buildPredicate( 11 | builder: CriteriaBuilder, 12 | path: Path<*>, 13 | fieldName: String, 14 | ops: SearchOperation?, 15 | value: Any? 16 | ): Predicate? { 17 | return when (ops) { 18 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Float) 19 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Float) 20 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Float) 21 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Float) 22 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 23 | } 24 | } 25 | 26 | override fun parse(value: String?, fieldClass: KClass): Any? { 27 | if (value == SearchOperation.NULL) return value 28 | return value?.toFloat() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import java.time.Instant 8 | import kotlin.reflect.KClass 9 | 10 | class InstantStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | return when (ops) { 19 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Instant) 20 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Instant) 21 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Instant) 22 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Instant) 23 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 24 | } 25 | } 26 | 27 | override fun parse(value: String?, fieldClass: KClass): Any? { 28 | if (value == SearchOperation.NULL) return value 29 | return Instant.parse(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import kotlin.reflect.KClass 8 | 9 | class IntStrategy : ParsingStrategy { 10 | override fun buildPredicate( 11 | builder: CriteriaBuilder, 12 | path: Path<*>, 13 | fieldName: String, 14 | ops: SearchOperation?, 15 | value: Any? 16 | ): Predicate? { 17 | return when (ops) { 18 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Int) 19 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Int) 20 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Int) 21 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Int) 22 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 23 | } 24 | } 25 | 26 | override fun parse(value: String?, fieldClass: KClass): Any? { 27 | if (value == SearchOperation.NULL) return value 28 | return value?.toInt() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import java.time.LocalDate 8 | import kotlin.reflect.KClass 9 | 10 | class LocalDateStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | return when (ops) { 19 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalDate) 20 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalDate) 21 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalDate) 22 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalDate) 23 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 24 | } 25 | } 26 | 27 | override fun parse(value: String?, fieldClass: KClass): Any? { 28 | if (value == SearchOperation.NULL) return value 29 | return LocalDate.parse(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import java.time.LocalDateTime 8 | import kotlin.reflect.KClass 9 | 10 | class LocalDateTimeStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | return when (ops) { 19 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalDateTime) 20 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalDateTime) 21 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalDateTime) 22 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalDateTime) 23 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 24 | } 25 | } 26 | 27 | override fun parse(value: String?, fieldClass: KClass): Any? { 28 | if (value == SearchOperation.NULL) return value 29 | return LocalDateTime.parse(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import jakarta.persistence.criteria.CriteriaBuilder 5 | import jakarta.persistence.criteria.Path 6 | import jakarta.persistence.criteria.Predicate 7 | import java.time.LocalTime 8 | import kotlin.reflect.KClass 9 | 10 | class LocalTimeStrategy : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | return when (ops) { 19 | SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalTime) 20 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalTime) 21 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalTime) 22 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalTime) 23 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 24 | } 25 | } 26 | 27 | override fun parse(value: String?, fieldClass: KClass): Any? { 28 | if (value == SearchOperation.NULL) return value 29 | return LocalTime.parse(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import com.sipios.springsearch.anotation.SearchSpec 5 | import jakarta.persistence.criteria.CriteriaBuilder 6 | import jakarta.persistence.criteria.Path 7 | import jakarta.persistence.criteria.Predicate 8 | import java.time.Duration 9 | import java.time.Instant 10 | import java.time.LocalDate 11 | import java.time.LocalDateTime 12 | import java.time.LocalTime 13 | import java.util.Date 14 | import java.util.UUID 15 | import kotlin.reflect.KClass 16 | import kotlin.reflect.full.isSubclassOf 17 | import org.springframework.http.HttpStatus 18 | import org.springframework.web.server.ResponseStatusException 19 | interface ParsingStrategy { 20 | /** 21 | * Method to parse the value specified to the corresponding strategy 22 | * 23 | * @param value Value used for the search 24 | * @param fieldClass Kotlin class of the referred field 25 | * @return Returns by default the value without any parsing 26 | */ 27 | fun parse(value: String?, fieldClass: KClass): Any? { 28 | return value 29 | } 30 | 31 | fun parse(value: List<*>?, fieldClass: KClass): Any? { 32 | return value?.map { parse(it.toString(), fieldClass) } 33 | } 34 | 35 | /** 36 | * Method to build the predicate 37 | * 38 | * @param builder Criteria object to build on 39 | * @param path Current path for predicate 40 | * @param fieldName Name of the field to be searched 41 | * @param ops Search operation to use 42 | * @param value Value used for the search 43 | * @return Returns a Predicate instance or null if the operation was not found 44 | */ 45 | fun buildPredicate( 46 | builder: CriteriaBuilder, 47 | path: Path<*>, 48 | fieldName: String, 49 | ops: SearchOperation?, 50 | value: Any? 51 | ): Predicate? { 52 | return when (ops) { 53 | SearchOperation.IN_ARRAY -> { 54 | val inClause: CriteriaBuilder.In = getInClause(builder, path, fieldName, value) 55 | inClause 56 | } 57 | 58 | SearchOperation.NOT_IN_ARRAY -> { 59 | val inClause: CriteriaBuilder.In = getInClause(builder, path, fieldName, value) 60 | builder.not(inClause) 61 | } 62 | 63 | SearchOperation.IS -> { 64 | if (value == SearchOperation.NULL) { 65 | builder.isNull(path.get(fieldName)) 66 | } else { 67 | // we should not call parent method for collection fields 68 | // so this makes no sense to search for EMPTY with a non-collection field 69 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, 70 | "Unsupported operation $ops $value for field $fieldName") 71 | } 72 | } 73 | SearchOperation.IS_NOT -> { 74 | if (value == SearchOperation.NULL) { 75 | builder.isNotNull(path.get(fieldName)) 76 | } else { 77 | // we should not call parent method for collection fields 78 | // so this makes no sense to search for NOT EMPTY with a non-collection field 79 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, 80 | "Unsupported operation $ops $value for field $fieldName") 81 | } 82 | } 83 | 84 | SearchOperation.EQUALS -> builder.equal(path.get(fieldName), value) 85 | SearchOperation.NOT_EQUALS -> builder.notEqual(path.get(fieldName), value) 86 | SearchOperation.STARTS_WITH -> builder.like(path[fieldName], "$value%") 87 | SearchOperation.ENDS_WITH -> builder.like(path[fieldName], "%$value") 88 | SearchOperation.CONTAINS -> builder.like((path.get(fieldName).`as`(String::class.java)), "%$value%") 89 | SearchOperation.DOESNT_START_WITH -> builder.notLike(path[fieldName], "$value%") 90 | SearchOperation.DOESNT_END_WITH -> builder.notLike(path[fieldName], "%$value") 91 | SearchOperation.DOESNT_CONTAIN -> builder.notLike( 92 | (path.get(fieldName).`as`(String::class.java)), 93 | "%$value%" 94 | ) 95 | 96 | else -> null 97 | } 98 | } 99 | 100 | fun getInClause( 101 | builder: CriteriaBuilder, 102 | path: Path<*>, 103 | fieldName: String, 104 | value: Any? 105 | ): CriteriaBuilder.In { 106 | val inClause: CriteriaBuilder.In = builder.`in`(path.get(fieldName)) 107 | val values = value as List<*> 108 | values.forEach { inClause.value(it) } 109 | return inClause 110 | } 111 | 112 | companion object { 113 | fun getStrategy(fieldClass: KClass, searchSpecAnnotation: SearchSpec, isCollectionField: Boolean): ParsingStrategy { 114 | return when { 115 | isCollectionField -> CollectionStrategy() 116 | fieldClass == Boolean::class -> BooleanStrategy() 117 | fieldClass == Date::class -> DateStrategy() 118 | fieldClass == Double::class -> DoubleStrategy() 119 | fieldClass == Float::class -> FloatStrategy() 120 | fieldClass == Int::class -> IntStrategy() 121 | fieldClass.isSubclassOf(Enum::class) -> EnumStrategy() 122 | fieldClass == Duration::class -> DurationStrategy() 123 | fieldClass == LocalDate::class -> LocalDateStrategy() 124 | fieldClass == LocalTime::class -> LocalTimeStrategy() 125 | fieldClass == LocalDateTime::class -> LocalDateTimeStrategy() 126 | fieldClass == Instant::class -> InstantStrategy() 127 | fieldClass == UUID::class -> UUIDStrategy() 128 | else -> StringStrategy(searchSpecAnnotation) 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/StringStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import com.sipios.springsearch.anotation.SearchSpec 5 | import jakarta.persistence.criteria.CriteriaBuilder 6 | import jakarta.persistence.criteria.Path 7 | import jakarta.persistence.criteria.Predicate 8 | import java.util.Locale 9 | 10 | data class StringStrategy(var searchSpecAnnotation: SearchSpec) : ParsingStrategy { 11 | override fun buildPredicate( 12 | builder: CriteriaBuilder, 13 | path: Path<*>, 14 | fieldName: String, 15 | ops: SearchOperation?, 16 | value: Any? 17 | ): Predicate? { 18 | if (value !is List<*>) { 19 | // if case-sensitive is enabled, we don't change the value 20 | val casedValue = if (searchSpecAnnotation.caseSensitiveFlag) { 21 | value.toString() 22 | } else { 23 | value.toString().lowercase(Locale.ROOT) 24 | } 25 | val casedField = if (searchSpecAnnotation.caseSensitiveFlag) { 26 | path[fieldName] 27 | } else { 28 | builder.lower(path[fieldName]) 29 | } 30 | return when (ops) { 31 | SearchOperation.STARTS_WITH -> builder.like(casedField, "$casedValue%") 32 | SearchOperation.ENDS_WITH -> builder.like(casedField, "%$casedValue") 33 | SearchOperation.CONTAINS -> builder.like((casedField), "%$casedValue%") 34 | SearchOperation.DOESNT_START_WITH -> builder.notLike(casedField, "$casedValue%") 35 | SearchOperation.DOESNT_END_WITH -> builder.notLike(casedField, "%$casedValue") 36 | SearchOperation.DOESNT_CONTAIN -> builder.notLike(casedField, "%$casedValue%") 37 | SearchOperation.GREATER_THAN -> builder.greaterThan(casedField, casedValue) 38 | SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(casedField, casedValue) 39 | SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], casedValue) 40 | SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], casedValue) 41 | else -> super.buildPredicate(builder, path, fieldName, ops, value) 42 | } 43 | } 44 | return super.buildPredicate(builder, path, fieldName, ops, value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch.strategies 2 | 3 | import com.sipios.springsearch.SearchOperation 4 | import java.util.UUID 5 | import kotlin.reflect.KClass 6 | 7 | class UUIDStrategy : ParsingStrategy { 8 | override fun parse(value: String?, fieldClass: KClass): Any? { 9 | if (value == SearchOperation.NULL) return value 10 | return UUID.fromString(value) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.sipios.springsearch.configuration.ResolverConf 3 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodo-fintech/spring-search/c4b404dc6cf22b540d3e7f6211e557f5d83b6c61/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/Author.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import jakarta.persistence.CascadeType 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.GeneratedValue 7 | import jakarta.persistence.GenerationType 8 | import jakarta.persistence.Id 9 | import jakarta.persistence.OneToMany 10 | import jakarta.persistence.Table 11 | 12 | @Entity 13 | @Table(name = "author") 14 | open class Author { 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | @Column(name = "id", nullable = false) 18 | open var id: Long? = null 19 | 20 | @OneToMany(mappedBy = "author", cascade = [CascadeType.ALL], orphanRemoval = true) 21 | open var books: MutableList = mutableListOf() 22 | 23 | @Column(name = "name") 24 | open var name: String? = null 25 | 26 | fun addBook(book: Book) { 27 | books.add(book) 28 | book.author = this 29 | } 30 | 31 | fun removeBook(book: Book) { 32 | books.remove(book) 33 | book.author = null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/AuthorRepository.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource 6 | 7 | @RepositoryRestResource(path = "author") 8 | interface AuthorRepository : CrudRepository, JpaSpecificationExecutor 9 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/Book.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import jakarta.persistence.Column 4 | import jakarta.persistence.Entity 5 | import jakarta.persistence.GeneratedValue 6 | import jakarta.persistence.GenerationType 7 | import jakarta.persistence.Id 8 | import jakarta.persistence.JoinColumn 9 | import jakarta.persistence.ManyToOne 10 | import jakarta.persistence.Table 11 | import org.hibernate.proxy.HibernateProxy 12 | 13 | @Entity 14 | @Table(name = "book") 15 | open class Book { 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.AUTO) 18 | @Column(name = "id", nullable = false) 19 | open var id: Long? = null 20 | 21 | @ManyToOne 22 | @JoinColumn(name = "author_id") 23 | open var author: Author? = null 24 | 25 | @Column(name = "title") 26 | open var title: String? = null 27 | 28 | final override fun equals(other: Any?): Boolean { 29 | if (this === other) return true 30 | if (other == null) return false 31 | val oEffectiveClass = 32 | if (other is HibernateProxy) other.hibernateLazyInitializer.persistentClass else other.javaClass 33 | val thisEffectiveClass = 34 | if (this is HibernateProxy) this.hibernateLazyInitializer.persistentClass else this.javaClass 35 | if (thisEffectiveClass != oEffectiveClass) return false 36 | other as Book 37 | 38 | return id != null && id == other.id 39 | } 40 | 41 | final override fun hashCode(): Int = 42 | if (this is HibernateProxy) this.hibernateLazyInitializer.persistentClass.hashCode() else javaClass.hashCode() 43 | } 44 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/SpringSearchApplication.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class SpringSearchApplication 8 | 9 | fun main() { 10 | runApplication() 11 | } 12 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import com.sipios.springsearch.anotation.SearchSpec 5 | import java.time.Duration 6 | import java.time.Instant 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.util.Date 11 | import java.util.UUID 12 | import org.junit.jupiter.api.Assertions 13 | import org.junit.jupiter.api.Test 14 | import org.springframework.beans.factory.annotation.Autowired 15 | import org.springframework.boot.test.context.SpringBootTest 16 | import org.springframework.transaction.annotation.Transactional 17 | import org.springframework.web.server.ResponseStatusException 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [SpringSearchApplication::class]) 20 | @Transactional 21 | class SpringSearchApplicationTest { 22 | @Autowired 23 | lateinit var userRepository: UsersRepository 24 | 25 | @Autowired 26 | lateinit var authorRepository: AuthorRepository 27 | 28 | @Test 29 | fun run() { 30 | } 31 | 32 | @Test 33 | fun canAddUsers() { 34 | userRepository.save(Users()) 35 | 36 | Assertions.assertEquals(1, userRepository.findAll().count()) 37 | } 38 | 39 | @Test 40 | fun canGetUserWithId() { 41 | val userId = userRepository.save(Users()).userId 42 | userRepository.save(Users()) 43 | 44 | val specification = SpecificationsBuilder( 45 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 46 | ).withSearch("userId:$userId").build() 47 | Assertions.assertEquals(userId, userRepository.findAll(specification).get(0).userId) 48 | } 49 | 50 | @Test 51 | fun canGetUserWithName() { 52 | val aliceId = userRepository.save(Users(userFirstName = "Alice")).userId 53 | userRepository.save(Users(userFirstName = "Bob")) 54 | 55 | val specification = SpecificationsBuilder( 56 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 57 | ).withSearch("userFirstName:Alice").build() 58 | Assertions.assertEquals(aliceId, userRepository.findAll(specification).get(0).userId) 59 | } 60 | 61 | @Test 62 | fun canGetUserWithFirstNameAndLastName() { 63 | val aliceId = userRepository.save(Users(userFirstName = "Alice", userLastName = "One")).userId 64 | userRepository.save(Users(userFirstName = "Alice", userLastName = "Two")) 65 | userRepository.save(Users(userFirstName = "Bob", userLastName = "One")) 66 | userRepository.save(Users(userFirstName = "Bob", userLastName = "Two")) 67 | 68 | val specification = SpecificationsBuilder( 69 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 70 | ).withSearch("userFirstName:Alice AND userLastName:One").build() 71 | Assertions.assertEquals(aliceId, userRepository.findAll(specification).get(0).userId) 72 | } 73 | 74 | @Test 75 | fun canGetUserWithFrenchName() { 76 | val edouardProstId = userRepository.save(Users(userFirstName = "Édouard", userLastName = "Pröst")).userId 77 | 78 | val specification = SpecificationsBuilder( 79 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 80 | ).withSearch("userFirstName:Édouard AND userLastName:Pröst").build() 81 | Assertions.assertEquals(edouardProstId, userRepository.findAll(specification).get(0).userId) 82 | } 83 | 84 | @Test 85 | fun canGetUserWithChineseName() { 86 | val sunDemingId = userRepository.save(Users(userFirstName = "孫德明")).userId 87 | 88 | val specification = SpecificationsBuilder( 89 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 90 | ).withSearch("userFirstName:孫德明").build() 91 | Assertions.assertEquals(sunDemingId, userRepository.findAll(specification).get(0).userId) 92 | } 93 | 94 | @Test 95 | fun canGetUserWithChineseNameNoEncoding() { 96 | val sunDemingId = userRepository.save(Users(userFirstName = "孫德明")).userId 97 | 98 | val specification = SpecificationsBuilder( 99 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 100 | ).withSearch("userFirstName:孫德明").build() 101 | Assertions.assertEquals(sunDemingId, userRepository.findAll(specification).get(0).userId) 102 | } 103 | 104 | @Test 105 | fun canGetUserWithSpecialCharactersName() { 106 | val hackermanId = userRepository.save(Users(userFirstName = "&@#*\"''^^^\$``%=+§__hack3rman__")).userId 107 | 108 | val specification = SpecificationsBuilder( 109 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 110 | ).withSearch("userFirstName:&@#*\"''^^^\$``%=+§__hack3rman__").build() 111 | Assertions.assertEquals(hackermanId, userRepository.findAll(specification).get(0).userId) 112 | } 113 | 114 | @Test 115 | fun canGetUserWithSpaceInNameWithString() { 116 | val robertJuniorId = userRepository.save(Users(userFirstName = "robert junior")).userId 117 | 118 | val specification = SpecificationsBuilder( 119 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 120 | ).withSearch("userFirstName:'robert junior'").build() 121 | Assertions.assertEquals(robertJuniorId, userRepository.findAll(specification).get(0).userId) 122 | } 123 | 124 | @Test 125 | fun canGetUsersWithPartialStartingName() { 126 | val robertId = userRepository.save(Users(userFirstName = "robert")).userId 127 | val robertaId = userRepository.save(Users(userFirstName = "roberta")).userId 128 | userRepository.save(Users(userFirstName = "robot")) 129 | userRepository.save(Users(userFirstName = "röbert")) 130 | 131 | val specification = SpecificationsBuilder( 132 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 133 | ).withSearch("userFirstName:robe*").build() 134 | val robeUsers = userRepository.findAll(specification) 135 | Assertions.assertTrue(setOf(robertId, robertaId) == robeUsers.map { user -> user.userId }.toSet()) 136 | } 137 | 138 | @Test 139 | fun canGetUsersWithPartialEndingName() { 140 | val robertId = userRepository.save(Users(userFirstName = "robert")).userId 141 | val robertaId = userRepository.save(Users(userFirstName = "roubert")).userId 142 | userRepository.save(Users(userFirstName = "robot")) 143 | userRepository.save(Users(userFirstName = "röbęrt")) 144 | 145 | val specification = SpecificationsBuilder( 146 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 147 | ).withSearch("userFirstName:*ert").build() 148 | val robeUsers = userRepository.findAll(specification) 149 | Assertions.assertTrue(setOf(robertId, robertaId) == robeUsers.map { user -> user.userId }.toSet()) 150 | } 151 | 152 | @Test 153 | fun canGetUsersWithPartialNameAndSpecialCharacter() { 154 | val robertId = userRepository.save(Users(userFirstName = "rob*rt")).userId 155 | val robertaId = userRepository.save(Users(userFirstName = "rob*rta")).userId 156 | userRepository.save(Users(userFirstName = "robot")) 157 | userRepository.save(Users(userFirstName = "röb*rt")) 158 | 159 | val specification = SpecificationsBuilder( 160 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 161 | ).withSearch("userFirstName:rob**").build() 162 | val robeUsers = userRepository.findAll(specification) 163 | Assertions.assertTrue(setOf(robertId, robertaId) == robeUsers.map { user -> user.userId }.toSet()) 164 | } 165 | 166 | @Test 167 | fun canGetUsersWithPartialNameContaining() { 168 | val robertId = userRepository.save(Users(userFirstName = "Robert")).userId 169 | val robertaId = userRepository.save(Users(userFirstName = "Roberta")).userId 170 | val toborobeId = userRepository.save(Users(userFirstName = "Toborobe")).userId 171 | val obertaId = userRepository.save(Users(userFirstName = "oberta")).userId 172 | userRepository.save(Users(userFirstName = "Robot")) 173 | userRepository.save(Users(userFirstName = "Röbert")) 174 | 175 | val specification = SpecificationsBuilder( 176 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 177 | ).withSearch("userFirstName:*obe*").build() 178 | val specificationUsers = userRepository.findAll(specification) 179 | Assertions.assertTrue( 180 | setOf( 181 | robertId, 182 | robertaId, 183 | toborobeId, 184 | obertaId 185 | ) == specificationUsers.map { user -> user.userId }.toSet() 186 | ) 187 | } 188 | 189 | @Test 190 | fun canGetUsersWithPartialNameContainingWithSpecialCharacter() { 191 | val robertId = userRepository.save(Users(userFirstName = "Rob*rt")).userId 192 | val robertaId = userRepository.save(Users(userFirstName = "rob*rta")).userId 193 | val lobertaId = userRepository.save(Users(userFirstName = "Lob*rta")).userId 194 | val tobertaId = userRepository.save(Users(userFirstName = "Tob*rta")).userId 195 | userRepository.save(Users(userFirstName = "robot")) 196 | userRepository.save(Users(userFirstName = "röb*rt")) 197 | 198 | val specification = SpecificationsBuilder( 199 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 200 | ).withSearch("userFirstName:*ob**").build() 201 | val specificationUsers = userRepository.findAll(specification) 202 | Assertions.assertTrue( 203 | setOf( 204 | robertId, 205 | robertaId, 206 | lobertaId, 207 | tobertaId 208 | ) == specificationUsers.map { user -> user.userId }.toSet() 209 | ) 210 | } 211 | 212 | @Test 213 | fun canGetUsersWithPartialNameContainingSpecialCharacterUsingSimpleString() { 214 | val robertId = userRepository.save(Users(userFirstName = "Rob*rt")).userId 215 | val robertaId = userRepository.save(Users(userFirstName = "rob*rta")).userId 216 | val lobertaId = userRepository.save(Users(userFirstName = "Lob*rta")).userId 217 | val tobertaId = userRepository.save(Users(userFirstName = "Tob*rta")).userId 218 | userRepository.save(Users(userFirstName = "robot")) 219 | userRepository.save(Users(userFirstName = "röb*rt")) 220 | 221 | val specification = SpecificationsBuilder( 222 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 223 | ).withSearch("userFirstName:'*ob**'").build() 224 | val specificationUsers = userRepository.findAll(specification) 225 | Assertions.assertTrue( 226 | setOf( 227 | robertId, 228 | robertaId, 229 | lobertaId, 230 | tobertaId 231 | ) == specificationUsers.map { user -> user.userId }.toSet() 232 | ) 233 | } 234 | 235 | @Test 236 | fun canGetUsersWithPartialNameContainingWithSpecialCharacterUsingDoubleString() { 237 | val robertId = userRepository.save(Users(userFirstName = "Rob*rt")).userId 238 | val robertaId = userRepository.save(Users(userFirstName = "rob*rta")).userId 239 | val lobertaId = userRepository.save(Users(userFirstName = "Lob*rta")).userId 240 | val tobertaId = userRepository.save(Users(userFirstName = "Tob*rta")).userId 241 | userRepository.save(Users(userFirstName = "robot")) 242 | userRepository.save(Users(userFirstName = "röb*rt")) 243 | 244 | val specification = SpecificationsBuilder( 245 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 246 | ).withSearch("userFirstName:\"*ob**\"").build() 247 | val specificationUsers = userRepository.findAll(specification) 248 | Assertions.assertTrue( 249 | setOf( 250 | robertId, 251 | robertaId, 252 | lobertaId, 253 | tobertaId 254 | ) == specificationUsers.map { user -> user.userId }.toSet() 255 | ) 256 | } 257 | 258 | @Test 259 | fun canGetUsersNotContaining() { 260 | val lobertaId = userRepository.save(Users(userFirstName = "Lobérta")).userId 261 | val tobertaId = userRepository.save(Users(userFirstName = "Toberta")).userId 262 | val robotId = userRepository.save(Users(userFirstName = "robot")).userId 263 | val roobertId = userRepository.save(Users(userFirstName = "röbert")).userId 264 | userRepository.save(Users(userFirstName = "Robèrt")).userId 265 | userRepository.save(Users(userFirstName = "robèrta")).userId 266 | 267 | val specification = SpecificationsBuilder( 268 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 269 | ).withSearch("userFirstName!*è*").build() 270 | val specificationUsers = userRepository.findAll(specification) 271 | Assertions.assertTrue( 272 | setOf( 273 | lobertaId, 274 | tobertaId, 275 | robotId, 276 | roobertId 277 | ) == specificationUsers.map { user -> user.userId }.toSet() 278 | ) 279 | } 280 | 281 | @Test 282 | fun canGetUsersNotStartingWith() { 283 | val aliceId = userRepository.save(Users(userFirstName = "Alice")).userId 284 | val aliceId2 = userRepository.save(Users(userFirstName = "alice")).userId 285 | val bobId = userRepository.save(Users(userFirstName = "bob")).userId 286 | userRepository.save(Users(userFirstName = "Bob")) 287 | 288 | val specification = SpecificationsBuilder( 289 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 290 | ).withSearch("userFirstName!B*").build() 291 | val specificationUsers = userRepository.findAll(specification) 292 | Assertions.assertTrue(setOf(aliceId, aliceId2, bobId) == specificationUsers.map { user -> user.userId }.toSet()) 293 | } 294 | 295 | @Test 296 | fun canGetUsersNotEndingWith() { 297 | val bobId = userRepository.save(Users(userFirstName = "bob")).userId 298 | val alicEId = userRepository.save(Users(userFirstName = "alicE")).userId 299 | val boBId = userRepository.save(Users(userFirstName = "boB")).userId 300 | userRepository.save(Users(userFirstName = "alice")) 301 | 302 | val specification = SpecificationsBuilder( 303 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 304 | ).withSearch("userFirstName!*e").build() 305 | val specificationUsers = userRepository.findAll(specification) 306 | Assertions.assertTrue(setOf(boBId, alicEId, bobId) == specificationUsers.map { user -> user.userId }.toSet()) 307 | } 308 | 309 | @Test 310 | fun canGetUserWithBigFamily() { 311 | val userWith5ChildrenId = userRepository.save(Users(userChildrenNumber = 5)).userId 312 | val userWith6ChildrenId = userRepository.save(Users(userChildrenNumber = 6)).userId 313 | val user2With5ChildrenId = userRepository.save(Users(userChildrenNumber = 5)).userId 314 | userRepository.save(Users(userChildrenNumber = 1)) 315 | userRepository.save(Users(userChildrenNumber = 2)) 316 | userRepository.save(Users(userChildrenNumber = 4)) 317 | userRepository.save(Users(userChildrenNumber = 2)) 318 | 319 | val specification = SpecificationsBuilder( 320 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 321 | ).withSearch("userChildrenNumber>4").build() 322 | val specificationUsers = userRepository.findAll(specification) 323 | Assertions.assertTrue( 324 | setOf( 325 | user2With5ChildrenId, 326 | userWith5ChildrenId, 327 | userWith6ChildrenId 328 | ) == specificationUsers.map { user -> user.userId }.toSet() 329 | ) 330 | } 331 | 332 | @Test 333 | fun canGetUserWithSmallFamily() { 334 | userRepository.save(Users(userChildrenNumber = 5)) 335 | userRepository.save(Users(userChildrenNumber = 6)) 336 | userRepository.save(Users(userChildrenNumber = 5)) 337 | val userWith1ChildrenId = userRepository.save(Users(userChildrenNumber = 1)).userId 338 | val userWith2ChildrenId = userRepository.save(Users(userChildrenNumber = 2)).userId 339 | userRepository.save(Users(userChildrenNumber = 4)) 340 | val user2With2ChildrenId = userRepository.save(Users(userChildrenNumber = 2)).userId 341 | 342 | val specification = SpecificationsBuilder( 343 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 344 | ).withSearch("userChildrenNumber<4").build() 345 | val specificationUsers = userRepository.findAll(specification) 346 | Assertions.assertTrue( 347 | setOf( 348 | user2With2ChildrenId, 349 | userWith1ChildrenId, 350 | userWith2ChildrenId 351 | ) == specificationUsers.map { user -> user.userId }.toSet() 352 | ) 353 | } 354 | 355 | @Test 356 | fun canGetUserWithChildrenEquals() { 357 | val user1With4ChildrenId = userRepository.save(Users(userChildrenNumber = 4)).userId 358 | val user2With4ChildrenId = userRepository.save(Users(userChildrenNumber = 4)).userId 359 | val user3With4ChildrenId = userRepository.save(Users(userChildrenNumber = 4)).userId 360 | userRepository.save(Users(userChildrenNumber = 5)) 361 | userRepository.save(Users(userChildrenNumber = 1)) 362 | userRepository.save(Users(userChildrenNumber = 2)) 363 | userRepository.save(Users(userChildrenNumber = 6)) 364 | userRepository.save(Users(userChildrenNumber = 2)) 365 | userRepository.save(Users(userChildrenNumber = 5)) 366 | 367 | val specification = SpecificationsBuilder( 368 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 369 | ).withSearch("userChildrenNumber:4").build() 370 | val specificationUsers = userRepository.findAll(specification) 371 | Assertions.assertTrue( 372 | setOf( 373 | user1With4ChildrenId, 374 | user2With4ChildrenId, 375 | user3With4ChildrenId 376 | ) == specificationUsers.map { user -> user.userId }.toSet() 377 | ) 378 | } 379 | 380 | @Test 381 | fun canGetUserWithChildrenNotEquals() { 382 | val userWith5ChildrenId = userRepository.save(Users(userChildrenNumber = 5)).userId 383 | val userWith1ChildId = userRepository.save(Users(userChildrenNumber = 1)).userId 384 | val userWith6ChildrenId = userRepository.save(Users(userChildrenNumber = 6)).userId 385 | userRepository.save(Users(userChildrenNumber = 2)) 386 | userRepository.save(Users(userChildrenNumber = 2)) 387 | 388 | val specification = SpecificationsBuilder( 389 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 390 | ).withSearch("userChildrenNumber!2").build() 391 | val specificationUsers = userRepository.findAll(specification) 392 | Assertions.assertTrue( 393 | setOf( 394 | userWith1ChildId, 395 | userWith5ChildrenId, 396 | userWith6ChildrenId 397 | ) == specificationUsers.map { user -> user.userId }.toSet() 398 | ) 399 | } 400 | 401 | @Test 402 | fun canGetUserWithSmallerSalary() { 403 | val smallerSalaryUserId = userRepository.save(Users(userSalary = 2223.3F)).userId 404 | val smallerSalaryUser2Id = userRepository.save(Users(userSalary = 1500.2F)).userId 405 | userRepository.save(Users(userSalary = 4000.0F)) 406 | userRepository.save(Users(userSalary = 2550.7F)) 407 | userRepository.save(Users(userSalary = 2300.0F)) 408 | 409 | val specification = SpecificationsBuilder( 410 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 411 | ).withSearch("userSalary<2300").build() 412 | val specificationUsers = userRepository.findAll(specification) 413 | Assertions.assertTrue( 414 | setOf( 415 | smallerSalaryUserId, 416 | smallerSalaryUser2Id 417 | ) == specificationUsers.map { user -> user.userId }.toSet() 418 | ) 419 | } 420 | 421 | @Test 422 | fun canGetUserWithHigherFloatSalary() { 423 | val higherSalaryUserId = userRepository.save(Users(userSalary = 4000.1F)).userId 424 | val higherSalaryUser2Id = userRepository.save(Users(userSalary = 5350.7F)).userId 425 | userRepository.save(Users(userSalary = 2323.3F)) 426 | userRepository.save(Users(userSalary = 1500.2F)) 427 | 428 | val specification = SpecificationsBuilder( 429 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 430 | ).withSearch("userSalary>4000.001").build() 431 | val specificationUsers = userRepository.findAll(specification) 432 | Assertions.assertTrue( 433 | setOf( 434 | higherSalaryUserId, 435 | higherSalaryUser2Id 436 | ) == specificationUsers.map { user -> user.userId }.toSet() 437 | ) 438 | } 439 | 440 | @Test 441 | fun canGetUserWithMedianSalary() { 442 | val medianUserId = userRepository.save(Users(userSalary = 2323.3F)).userId 443 | userRepository.save(Users(userSalary = 1500.2F)) 444 | userRepository.save(Users(userSalary = 4000.1F)) 445 | userRepository.save(Users(userSalary = 5350.7F)) 446 | 447 | val specification = SpecificationsBuilder( 448 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 449 | ).withSearch("userSalary<4000.1 AND userSalary>1500.2").build() 450 | val specificationUsers = userRepository.findAll(specification) 451 | Assertions.assertTrue(setOf(medianUserId) == specificationUsers.map { user -> user.userId }.toSet()) 452 | } 453 | 454 | @Test 455 | fun canGetUsersWithAgeHigher() { 456 | val olderUserId = userRepository.save(Users(userAgeInSeconds = 23222223.3)).userId 457 | userRepository.save(Users(userAgeInSeconds = 23222223.2)) 458 | userRepository.save(Users(userAgeInSeconds = 23222223.0)) 459 | 460 | val specification = SpecificationsBuilder( 461 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 462 | ).withSearch("userAgeInSeconds>23222223.2").build() 463 | val specificationUsers = userRepository.findAll(specification) 464 | Assertions.assertTrue(setOf(olderUserId) == specificationUsers.map { user -> user.userId }.toSet()) 465 | } 466 | 467 | @Test 468 | fun canGetUsersWithAgeLower() { 469 | val youngerUserId = userRepository.save(Users(userAgeInSeconds = 23222223.0)).userId 470 | userRepository.save(Users(userAgeInSeconds = 23222223.2)) 471 | userRepository.save(Users(userAgeInSeconds = 23222223.3)) 472 | 473 | val specification = SpecificationsBuilder( 474 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 475 | ).withSearch("userAgeInSeconds<23222223.2").build() 476 | val specificationUsers = userRepository.findAll(specification) 477 | Assertions.assertTrue(setOf(youngerUserId) == specificationUsers.map { user -> user.userId }.toSet()) 478 | } 479 | 480 | @Test 481 | fun canGetUsersWithAgeEqual() { 482 | val middleUserId = userRepository.save(Users(userAgeInSeconds = 23222223.2)).userId 483 | userRepository.save(Users(userAgeInSeconds = 23222223.3)) 484 | userRepository.save(Users(userAgeInSeconds = 23222223.0)) 485 | 486 | val specification = SpecificationsBuilder( 487 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 488 | ).withSearch("userAgeInSeconds:23222223.2").build() 489 | val specificationUsers = userRepository.findAll(specification) 490 | Assertions.assertTrue(setOf(middleUserId) == specificationUsers.map { user -> user.userId }.toSet()) 491 | } 492 | 493 | @Test 494 | fun canGetUserWithParentheses() { 495 | val userOneWithHigherSalaryId = userRepository.save(Users(userSalary = 1500.2F, userLastName = "One")).userId 496 | val userTwoWithHigherSalaryId = userRepository.save(Users(userSalary = 1500.2F, userLastName = "Two")).userId 497 | userRepository.save(Users(userSalary = 1500.1F, userLastName = "One")) 498 | userRepository.save(Users(userSalary = 1500.1F, userLastName = "Two")) 499 | userRepository.save(Users(userSalary = 1500.1F, userLastName = "Three")) 500 | userRepository.save(Users(userSalary = 1500.2F, userLastName = "Three")) 501 | 502 | val specification = SpecificationsBuilder( 503 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 504 | ).withSearch("userSalary>1500.1 AND ( userLastName:One OR userLastName:Two )").build() 505 | val specificationUsers = userRepository.findAll(specification) 506 | Assertions.assertTrue( 507 | setOf( 508 | userOneWithHigherSalaryId, 509 | userTwoWithHigherSalaryId 510 | ) == specificationUsers.map { user -> user.userId }.toSet() 511 | ) 512 | } 513 | 514 | @Test 515 | fun canGetUsersWithInterlinkedConditions() { 516 | val userOneWithSmallerSalaryId = userRepository.save(Users(userSalary = 1501F, userLastName = "One")).userId 517 | val userOeId = userRepository.save(Users(userSalary = 1501F, userLastName = "Oe")).userId 518 | userRepository.save(Users(userSalary = 1501F, userLastName = "One one")) 519 | userRepository.save(Users(userSalary = 1501F, userLastName = "Oneone")) 520 | userRepository.save(Users(userSalary = 1501F, userLastName = "O n e")) 521 | userRepository.save(Users(userSalary = 1502F, userLastName = "One")) 522 | 523 | val specification = SpecificationsBuilder( 524 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 525 | ).withSearch("userSalary<1502 AND ( ( userLastName:One OR userLastName:one ) OR userLastName!*n* )").build() 526 | val specificationUsers = userRepository.findAll(specification) 527 | Assertions.assertTrue( 528 | setOf( 529 | userOneWithSmallerSalaryId, 530 | userOeId 531 | ) == specificationUsers.map { user -> user.userId }.toSet() 532 | ) 533 | } 534 | 535 | @Test 536 | fun canGetUsersWithInterlinkedConditionsNoSpaces() { 537 | val userOneWithSmallerSalaryId = userRepository.save(Users(userSalary = 1501F, userLastName = "One")).userId 538 | val userOeId = userRepository.save(Users(userSalary = 1501F, userLastName = "Oe")).userId 539 | userRepository.save(Users(userSalary = 1501F, userLastName = "One one")) 540 | userRepository.save(Users(userSalary = 1501F, userLastName = "Oneone")) 541 | userRepository.save(Users(userSalary = 1501F, userLastName = "O n e")) 542 | userRepository.save(Users(userSalary = 1502F, userLastName = "One")) 543 | 544 | val specification = SpecificationsBuilder( 545 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 546 | ).withSearch("userSalary<1502 AND ((userLastName:One OR userLastName:one) OR userLastName!*n*)").build() 547 | val specificationUsers = userRepository.findAll(specification) 548 | Assertions.assertTrue( 549 | setOf( 550 | userOneWithSmallerSalaryId, 551 | userOeId 552 | ) == specificationUsers.map { user -> user.userId }.toSet() 553 | ) 554 | } 555 | 556 | @Test 557 | fun canGetUsersByBoolean() { 558 | userRepository.save(Users(isAdmin = true)) 559 | userRepository.save(Users(isAdmin = false)) 560 | 561 | val specification = SpecificationsBuilder( 562 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 563 | ).withSearch("isAdmin:true").build() 564 | val specificationUsers = userRepository.findAll(specification) 565 | Assertions.assertEquals(1, specificationUsers.size) 566 | } 567 | 568 | @Test 569 | fun canGetUsersEarlierThanDate() { 570 | val sdf = StdDateFormat() 571 | userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) 572 | userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) 573 | 574 | val specification = SpecificationsBuilder( 575 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 576 | ).withSearch("createdAt<'2019-01-02'").build() 577 | val specificationUsers = userRepository.findAll(specification) 578 | Assertions.assertEquals(1, specificationUsers.size) 579 | } 580 | 581 | @Test 582 | fun canGetUsersAfterDate() { 583 | val sdf = StdDateFormat() 584 | userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) 585 | userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) 586 | 587 | var specification = SpecificationsBuilder( 588 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 589 | ).withSearch("createdAt>'2019-01-02'").build() 590 | var specificationUsers = userRepository.findAll(specification) 591 | Assertions.assertEquals(1, specificationUsers.size) 592 | 593 | specification = SpecificationsBuilder( 594 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 595 | ).withSearch("createdAt>'2019-01-04'").build() 596 | specificationUsers = userRepository.findAll(specification) 597 | Assertions.assertEquals(0, specificationUsers.size) 598 | } 599 | 600 | @Test 601 | fun canGetUsersAfterEqualDate() { 602 | val sdf = StdDateFormat() 603 | userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) 604 | userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) 605 | 606 | var specification = SpecificationsBuilder( 607 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 608 | ).withSearch("createdAt>:'2019-01-01'").build() 609 | var specificationUsers = userRepository.findAll(specification) 610 | Assertions.assertEquals(2, specificationUsers.size) 611 | 612 | specification = SpecificationsBuilder( 613 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 614 | ).withSearch("createdAt>:'2019-01-04'").build() 615 | specificationUsers = userRepository.findAll(specification) 616 | Assertions.assertEquals(0, specificationUsers.size) 617 | } 618 | 619 | @Test 620 | fun canGetUsersEarlierEqualDate() { 621 | val sdf = StdDateFormat() 622 | userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) 623 | userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) 624 | 625 | var specification = SpecificationsBuilder( 626 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 627 | ).withSearch("createdAt<:'2019-01-01'").build() 628 | var specificationUsers = userRepository.findAll(specification) 629 | Assertions.assertEquals(1, specificationUsers.size) 630 | 631 | specification = SpecificationsBuilder( 632 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 633 | ).withSearch("createdAt<:'2019-01-03'").build() 634 | specificationUsers = userRepository.findAll(specification) 635 | Assertions.assertEquals(2, specificationUsers.size) 636 | } 637 | 638 | @Test 639 | fun canGetUsersAtPreciseDate() { 640 | val sdf = StdDateFormat() 641 | val date = sdf.parse("2019-01-01") 642 | userRepository.save(Users(createdAt = date)) 643 | 644 | val specification = SpecificationsBuilder( 645 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 646 | ).withSearch("createdAt:'${sdf.format(date)}'").build() 647 | val specificationUsers = userRepository.findAll(specification) 648 | Assertions.assertEquals(1, specificationUsers.size) 649 | } 650 | 651 | @Test 652 | fun canGetUsersAtPreciseDateNotEqual() { 653 | val sdf = StdDateFormat() 654 | val date = sdf.parse("2019-01-01") 655 | userRepository.save(Users(createdAt = date)) 656 | 657 | val specification = SpecificationsBuilder( 658 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 659 | ).withSearch("createdAt!'2019-01-02'").build() 660 | val specificationUsers = userRepository.findAll(specification) 661 | Assertions.assertEquals(1, specificationUsers.size) 662 | } 663 | 664 | @Test 665 | fun canGetUsersWithCaseInsensitiveLowerCaseSearch() { 666 | val robertId = userRepository.save(Users(userFirstName = "ROBERT")).userId 667 | val robertaId = userRepository.save(Users(userFirstName = "roberta")).userId 668 | userRepository.save(Users(userFirstName = "robot")) 669 | userRepository.save(Users(userFirstName = "röbęrt")) 670 | 671 | val specification = SpecificationsBuilder( 672 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 673 | ).withSearch("userFirstName:robe*").build() 674 | val robeUsers = userRepository.findAll(specification) 675 | Assertions.assertTrue(setOf(robertId, robertaId) == robeUsers.map { user -> user.userId }.toSet()) 676 | } 677 | 678 | @Test 679 | fun canGetUsersWithCaseInsensitiveUpperCaseSearch() { 680 | val robertId = userRepository.save(Users(userFirstName = "ROBERT")).userId 681 | val roubertId = userRepository.save(Users(userFirstName = "roubert")).userId 682 | userRepository.save(Users(userFirstName = "robot")) 683 | userRepository.save(Users(userFirstName = "röbęrt")) 684 | 685 | val specification = SpecificationsBuilder( 686 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 687 | ).withSearch("userFirstName:*ert").build() 688 | val robeUsers = userRepository.findAll(specification) 689 | Assertions.assertTrue(setOf(robertId, roubertId) == robeUsers.map { user -> user.userId }.toSet()) 690 | } 691 | 692 | @Test 693 | fun canGetUsersWithCaseInsensitiveContainsSearch() { 694 | val robertId = userRepository.save(Users(userFirstName = "ROBERT")).userId 695 | val roubertId = userRepository.save(Users(userFirstName = "roubert")).userId 696 | userRepository.save(Users(userFirstName = "robot")) 697 | userRepository.save(Users(userFirstName = "röbęrt")) 698 | 699 | val specification = SpecificationsBuilder( 700 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 701 | ).withSearch("userFirstName:*er*").build() 702 | val robeUsers = userRepository.findAll(specification) 703 | Assertions.assertTrue(setOf(robertId, roubertId) == robeUsers.map { user -> user.userId }.toSet()) 704 | } 705 | 706 | @Test 707 | fun canGetUsersWithCaseInsensitiveDoesntContainSearch() { 708 | userRepository.save(Users(userFirstName = "ROBERT")) 709 | val roubertId = userRepository.save(Users(userFirstName = "roubert")).userId 710 | userRepository.save(Users(userFirstName = "robot")) 711 | 712 | val specification = SpecificationsBuilder( 713 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 714 | ).withSearch("userFirstName!*rob*").build() 715 | val robeUsers = userRepository.findAll(specification) 716 | Assertions.assertTrue(setOf(roubertId) == robeUsers.map { user -> user.userId }.toSet()) 717 | } 718 | 719 | @Test 720 | fun canGetUsersWithCaseInsensitiveDoesntStartSearch() { 721 | userRepository.save(Users(userFirstName = "ROBERT")) 722 | val roubertId = userRepository.save(Users(userFirstName = "roubert")).userId 723 | userRepository.save(Users(userFirstName = "robot")) 724 | 725 | val specification = SpecificationsBuilder( 726 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 727 | ).withSearch("userFirstName!rob*").build() 728 | val robeUsers = userRepository.findAll(specification) 729 | Assertions.assertTrue(setOf(roubertId) == robeUsers.map { user -> user.userId }.toSet()) 730 | } 731 | 732 | @Test 733 | fun canGetUsersWithCaseInsensitiveDoesntEndSearch() { 734 | userRepository.save(Users(userFirstName = "ROBERT")) 735 | userRepository.save(Users(userFirstName = "roubert")) 736 | val robotId = userRepository.save(Users(userFirstName = "robot")).userId 737 | 738 | val specification = SpecificationsBuilder( 739 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 740 | ).withSearch("userFirstName!*rt").build() 741 | val robotUsers = userRepository.findAll(specification) 742 | Assertions.assertTrue(setOf(robotId) == robotUsers.map { user -> user.userId }.toSet()) 743 | } 744 | 745 | @Test 746 | fun canGetUsersWithUserTypeEqualSearch() { 747 | userRepository.save(Users(userFirstName = "Hamid", type = UserType.TEAM_MEMBER)) 748 | userRepository.save(Users(userFirstName = "Reza", type = UserType.TEAM_MEMBER)) 749 | userRepository.save(Users(userFirstName = "Ireh", type = UserType.TEAM_MEMBER)) 750 | userRepository.save(Users(userFirstName = "robot", type = UserType.ADMINISTRATOR)) 751 | 752 | val specification = SpecificationsBuilder( 753 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 754 | ).withSearch("type:ADMINISTRATOR").build() 755 | val robeUsers = userRepository.findAll(specification) 756 | Assertions.assertEquals(1, robeUsers.size) 757 | Assertions.assertEquals("robot", robeUsers[0].userFirstName) 758 | } 759 | 760 | @Test 761 | fun canGetUsersWithUserTypeNotEqualSearch() { 762 | userRepository.save(Users(userFirstName = "HamidReza", type = UserType.TEAM_MEMBER)) 763 | userRepository.save(Users(userFirstName = "Ireh", type = UserType.ADMINISTRATOR)) 764 | userRepository.save(Users(userFirstName = "robot", type = UserType.TEAM_MEMBER)) 765 | 766 | val specification = SpecificationsBuilder( 767 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 768 | ).withSearch("type!TEAM_MEMBER").build() 769 | val irehUsers = userRepository.findAll(specification) 770 | Assertions.assertEquals(1, irehUsers.size) 771 | Assertions.assertEquals("Ireh", irehUsers[0].userFirstName) 772 | } 773 | 774 | @Test 775 | fun canGetUsersWithUpdatedAtGreaterSearch() { 776 | userRepository.save(Users(userFirstName = "HamidReza", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 777 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 778 | 779 | val specification = SpecificationsBuilder( 780 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 781 | ).withSearch("updatedAt>'2020-01-11T09:20:30'").build() 782 | val robotUsers = userRepository.findAll(specification) 783 | Assertions.assertEquals(1, robotUsers.size) 784 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 785 | } 786 | 787 | @Test 788 | fun canGetUsersWithUpdateInstantAtGreaterSearch() { 789 | userRepository.save( 790 | Users( 791 | userFirstName = "HamidReza", 792 | updatedInstantAt = Instant.parse("2020-01-10T10:15:30Z") 793 | ) 794 | ) 795 | userRepository.save(Users(userFirstName = "robot", updatedInstantAt = Instant.parse("2020-01-11T10:20:30Z"))) 796 | 797 | val specification = SpecificationsBuilder( 798 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 799 | ).withSearch("updatedInstantAt>'2020-01-11T09:20:30Z'").build() 800 | val robotUsers = userRepository.findAll(specification) 801 | Assertions.assertEquals(1, robotUsers.size) 802 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 803 | } 804 | 805 | @Test 806 | fun canGetUsersWithUpdateInstantAtGreaterThanEqualSearch() { 807 | userRepository.save( 808 | Users( 809 | userFirstName = "john", 810 | updatedInstantAt = Instant.parse("2020-01-10T10:15:30Z") 811 | ) 812 | ) 813 | userRepository.save(Users(userFirstName = "robot", updatedInstantAt = Instant.parse("2020-01-11T10:20:30Z"))) 814 | 815 | val specification = SpecificationsBuilder( 816 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 817 | ).withSearch("updatedInstantAt>:'2020-01-11T09:20:30Z'").build() 818 | val robotUsers = userRepository.findAll(specification) 819 | Assertions.assertEquals(1, robotUsers.size) 820 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 821 | } 822 | 823 | @Test 824 | fun canGetUsersWithUpdateInstantAtLessThanEqualSearch() { 825 | userRepository.save( 826 | Users( 827 | userFirstName = "john", 828 | updatedInstantAt = Instant.parse("2020-01-10T10:15:30Z") 829 | ) 830 | ) 831 | userRepository.save(Users(userFirstName = "robot", updatedInstantAt = Instant.parse("2020-01-11T10:20:30Z"))) 832 | 833 | val specification = SpecificationsBuilder( 834 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 835 | ).withSearch("updatedInstantAt<:'2020-01-11T09:20:30Z'").build() 836 | val robotUsers = userRepository.findAll(specification) 837 | Assertions.assertEquals(1, robotUsers.size) 838 | Assertions.assertEquals("john", robotUsers[0].userFirstName) 839 | } 840 | 841 | @Test 842 | fun canGetUsersWithUpdatedAtLessSearch() { 843 | userRepository.save(Users(userFirstName = "HamidReza", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 844 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 845 | 846 | val specification = SpecificationsBuilder( 847 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 848 | ).withSearch("updatedAt<'2020-01-11T09:20:30'").build() 849 | val hamidrezaUsers = userRepository.findAll(specification) 850 | Assertions.assertEquals(1, hamidrezaUsers.size) 851 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 852 | } 853 | 854 | @Test 855 | fun canGetUsersWithUpdatedAtEqualSearch() { 856 | userRepository.save(Users(userFirstName = "HamidReza", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 857 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 858 | 859 | val specification = SpecificationsBuilder( 860 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 861 | ).withSearch("updatedAt:'2020-01-10T10:15:30'").build() 862 | val hamidrezaUsers = userRepository.findAll(specification) 863 | Assertions.assertEquals(1, hamidrezaUsers.size) 864 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 865 | } 866 | 867 | @Test 868 | fun canGetUsersWithUpdatedAtNotEqualSearch() { 869 | userRepository.save(Users(userFirstName = "HamidReza", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 870 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 871 | 872 | val specification = SpecificationsBuilder( 873 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 874 | ).withSearch("updatedAt!'2020-01-11T10:20:30'").build() 875 | val hamidrezaUsers = userRepository.findAll(specification) 876 | Assertions.assertEquals(1, hamidrezaUsers.size) 877 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 878 | } 879 | 880 | @Test 881 | fun canGetUsersWithUpdatedAtGreaterThanEqualSearch() { 882 | userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 883 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 884 | userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) 885 | val specification = SpecificationsBuilder( 886 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 887 | ).withSearch("updatedAt>:'2020-01-11T10:20:30'").build() 888 | val users = userRepository.findAll(specification) 889 | Assertions.assertEquals(2, users.size) 890 | Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) 891 | } 892 | 893 | @Test 894 | fun canGetUsersWithUpdatedAtLessThanEqualSearch() { 895 | userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 896 | userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) 897 | userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) 898 | val specification = SpecificationsBuilder( 899 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 900 | ).withSearch("updatedAt<:'2020-01-11T10:20:30'").build() 901 | val users = userRepository.findAll(specification) 902 | Assertions.assertEquals(2, users.size) 903 | Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) 904 | } 905 | 906 | @Test 907 | fun canGetUsersWithUpdatedDateAtGreaterSearch() { 908 | userRepository.save(Users(userFirstName = "HamidReza", updatedDateAt = LocalDate.parse("2020-01-10"))) 909 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 910 | 911 | val specification = SpecificationsBuilder( 912 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 913 | ).withSearch("updatedDateAt>'2020-01-10'").build() 914 | val robotUsers = userRepository.findAll(specification) 915 | Assertions.assertEquals(1, robotUsers.size) 916 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 917 | } 918 | 919 | @Test 920 | fun canGetUsersWithUpdatedDateAtLessSearch() { 921 | userRepository.save(Users(userFirstName = "HamidReza", updatedDateAt = LocalDate.parse("2020-01-10"))) 922 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 923 | 924 | val specification = SpecificationsBuilder( 925 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 926 | ).withSearch("updatedDateAt<'2020-01-11'").build() 927 | val hamidrezaUsers = userRepository.findAll(specification) 928 | Assertions.assertEquals(1, hamidrezaUsers.size) 929 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 930 | } 931 | 932 | @Test 933 | fun canGetUsersWithUpdatedDateAtEqualSearch() { 934 | userRepository.save(Users(userFirstName = "HamidReza", updatedDateAt = LocalDate.parse("2020-01-10"))) 935 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 936 | 937 | val specification = SpecificationsBuilder( 938 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 939 | ).withSearch("updatedDateAt:'2020-01-10'").build() 940 | val hamidrezaUsers = userRepository.findAll(specification) 941 | Assertions.assertEquals(1, hamidrezaUsers.size) 942 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 943 | } 944 | 945 | @Test 946 | fun canGetUsersWithUpdatedDateAtNotEqualSearch() { 947 | userRepository.save(Users(userFirstName = "HamidReza", updatedDateAt = LocalDate.parse("2020-01-10"))) 948 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 949 | 950 | val specification = SpecificationsBuilder( 951 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 952 | ).withSearch("updatedDateAt!'2020-01-11'").build() 953 | val hamidrezaUsers = userRepository.findAll(specification) 954 | Assertions.assertEquals(1, hamidrezaUsers.size) 955 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 956 | } 957 | 958 | @Test 959 | fun canGetUsersWithUpdatedDateAtLessThanEqualSearch() { 960 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 961 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 962 | userRepository.save(Users(userFirstName = "robot2", updatedDateAt = LocalDate.parse("2020-01-12"))) 963 | 964 | val specification = SpecificationsBuilder( 965 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 966 | ).withSearch("updatedDateAt<:'2020-01-11'").build() 967 | val users = userRepository.findAll(specification) 968 | Assertions.assertEquals(2, users.size) 969 | Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) 970 | } 971 | 972 | @Test 973 | fun canGetUsersWithUpdatedDateAtGreaterThanEqualSearch() { 974 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 975 | userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) 976 | userRepository.save(Users(userFirstName = "robot2", updatedDateAt = LocalDate.parse("2020-01-12"))) 977 | 978 | val specification = SpecificationsBuilder( 979 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 980 | ).withSearch("updatedDateAt>:'2020-01-11'").build() 981 | val users = userRepository.findAll(specification) 982 | Assertions.assertEquals(2, users.size) 983 | Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) 984 | } 985 | 986 | @Test 987 | fun canGetUsersWithUpdatedTimeAtGreaterSearch() { 988 | userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) 989 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 990 | 991 | val specification = SpecificationsBuilder( 992 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 993 | ).withSearch("updatedTimeAt>'10:15:30'").build() 994 | val robotUsers = userRepository.findAll(specification) 995 | Assertions.assertEquals(1, robotUsers.size) 996 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 997 | } 998 | 999 | @Test 1000 | fun canGetUsersWithUpdatedTimeAtLessSearch() { 1001 | userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) 1002 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 1003 | 1004 | val specification = SpecificationsBuilder( 1005 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1006 | ).withSearch("updatedTimeAt<'10:16:30'").build() 1007 | val hamidrezaUsers = userRepository.findAll(specification) 1008 | Assertions.assertEquals(1, hamidrezaUsers.size) 1009 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 1010 | } 1011 | 1012 | @Test 1013 | fun canGetUsersWithUpdatedTimeAtEqualSearch() { 1014 | userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) 1015 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 1016 | 1017 | val specification = SpecificationsBuilder( 1018 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1019 | ).withSearch("updatedTimeAt:'10:15:30'").build() 1020 | val hamidrezaUsers = userRepository.findAll(specification) 1021 | Assertions.assertEquals(1, hamidrezaUsers.size) 1022 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 1023 | } 1024 | 1025 | @Test 1026 | fun canGetUsersWithUpdatedTimeAtLessThanEqualSearch() { 1027 | userRepository.save(Users(userFirstName = "john", updatedTimeAt = LocalTime.parse("10:15:30"))) 1028 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 1029 | userRepository.save(Users(userFirstName = "robot2", updatedTimeAt = LocalTime.parse("10:25:30"))) 1030 | 1031 | val specification = SpecificationsBuilder( 1032 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1033 | ).withSearch("updatedTimeAt<:'10:20:30'").build() 1034 | val users = userRepository.findAll(specification) 1035 | Assertions.assertEquals(2, users.size) 1036 | Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) 1037 | } 1038 | 1039 | @Test 1040 | fun canGetUsersWithUpdatedTimeAtGreaterThanEqualSearch() { 1041 | userRepository.save(Users(userFirstName = "john", updatedTimeAt = LocalTime.parse("10:15:30"))) 1042 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 1043 | userRepository.save(Users(userFirstName = "robot2", updatedTimeAt = LocalTime.parse("10:25:30"))) 1044 | 1045 | val specification = SpecificationsBuilder( 1046 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1047 | ).withSearch("updatedTimeAt>:'10:20:30'").build() 1048 | val users = userRepository.findAll(specification) 1049 | Assertions.assertEquals(2, users.size) 1050 | Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) 1051 | } 1052 | 1053 | @Test 1054 | fun canGetUsersWithUpdatedTimeAtNotEqualSearch() { 1055 | userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) 1056 | userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) 1057 | 1058 | val specification = SpecificationsBuilder( 1059 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1060 | ).withSearch("updatedTimeAt!'10:20:30'").build() 1061 | val hamidrezaUsers = userRepository.findAll(specification) 1062 | Assertions.assertEquals(1, hamidrezaUsers.size) 1063 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 1064 | } 1065 | 1066 | @Test 1067 | fun canGetUsersWithDurationGreaterSearch() { 1068 | userRepository.save(Users(userFirstName = "HamidReza", validityDuration = Duration.parse("PT10H"))) 1069 | userRepository.save(Users(userFirstName = "robot", validityDuration = Duration.parse("PT15H"))) 1070 | 1071 | val specification = SpecificationsBuilder( 1072 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1073 | ).withSearch("validityDuration>'PT10H'").build() 1074 | val robotUsers = userRepository.findAll(specification) 1075 | Assertions.assertEquals(1, robotUsers.size) 1076 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 1077 | } 1078 | 1079 | @Test 1080 | fun canGetUsersWithDurationLessSearch() { 1081 | userRepository.save(Users(userFirstName = "HamidReza", validityDuration = Duration.parse("PT10H"))) 1082 | userRepository.save(Users(userFirstName = "robot", validityDuration = Duration.parse("PT15H"))) 1083 | 1084 | val specification = SpecificationsBuilder( 1085 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1086 | ).withSearch("validityDuration<'PT11H'").build() 1087 | val hamidrezaUsers = userRepository.findAll(specification) 1088 | Assertions.assertEquals(1, hamidrezaUsers.size) 1089 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 1090 | } 1091 | 1092 | @Test 1093 | fun canGetUsersWithDurationEqualSearch() { 1094 | userRepository.save(Users(userFirstName = "HamidReza", validityDuration = Duration.parse("PT10H"))) 1095 | userRepository.save(Users(userFirstName = "robot", validityDuration = Duration.parse("PT15H"))) 1096 | 1097 | val specification = SpecificationsBuilder( 1098 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1099 | ).withSearch("validityDuration:'PT10H'").build() 1100 | val hamidrezaUsers = userRepository.findAll(specification) 1101 | Assertions.assertEquals(1, hamidrezaUsers.size) 1102 | Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) 1103 | } 1104 | 1105 | @Test 1106 | fun canGetUsersWithDurationNotEqualSearch() { 1107 | userRepository.save(Users(userFirstName = "HamidReza", validityDuration = Duration.parse("PT10H"))) 1108 | userRepository.save(Users(userFirstName = "robot", validityDuration = Duration.parse("PT15H"))) 1109 | 1110 | val specification = SpecificationsBuilder( 1111 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1112 | ).withSearch("validityDuration!'PT10H'").build() 1113 | val robotUsers = userRepository.findAll(specification) 1114 | Assertions.assertEquals(1, robotUsers.size) 1115 | Assertions.assertEquals("robot", robotUsers[0].userFirstName) 1116 | } 1117 | 1118 | @Test 1119 | fun canGetUsersWithUUIDEqualSearch() { 1120 | val userUUID = UUID.randomUUID() 1121 | userRepository.save(Users(userFirstName = "Diego", uuid = userUUID)) 1122 | val specification = SpecificationsBuilder( 1123 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1124 | ).withSearch("uuid:'$userUUID'").build() 1125 | val robotUsers = userRepository.findAll(specification) 1126 | Assertions.assertEquals(1, robotUsers.size) 1127 | Assertions.assertEquals(userUUID, robotUsers[0].uuid) 1128 | } 1129 | 1130 | @Test 1131 | fun canGetUsersWithUUIDNotEqualSearch() { 1132 | val userUUID = UUID.randomUUID() 1133 | val user2UUID = UUID.randomUUID() 1134 | userRepository.save(Users(userFirstName = "Diego", uuid = userUUID)) 1135 | userRepository.save(Users(userFirstName = "Diego two", uuid = user2UUID)) 1136 | val specification = SpecificationsBuilder( 1137 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1138 | ).withSearch("uuid!'$userUUID'").build() 1139 | val robotUsers = userRepository.findAll(specification) 1140 | Assertions.assertEquals(1, robotUsers.size) 1141 | Assertions.assertEquals(user2UUID, robotUsers[0].uuid) 1142 | } 1143 | 1144 | @Test 1145 | fun canGetUsersWithNumberOfChildrenLessOrEqualSearch() { 1146 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1147 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1148 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1149 | val specification = SpecificationsBuilder( 1150 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1151 | ).withSearch("userChildrenNumber<:2").build() 1152 | val users = userRepository.findAll(specification) 1153 | Assertions.assertEquals(1, users.size) 1154 | Assertions.assertEquals("john", users[0].userFirstName) 1155 | } 1156 | 1157 | @Test 1158 | fun canGetUsersWithNumberOfChildrenGreaterOrEqualSearch() { 1159 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1160 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1161 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1162 | val specification = SpecificationsBuilder( 1163 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1164 | ).withSearch("userChildrenNumber>:3").build() 1165 | val users = userRepository.findAll(specification) 1166 | Assertions.assertEquals(2, users.size) 1167 | } 1168 | 1169 | @Test 1170 | fun canGetUsersWithNumberOfChildrenLessSearch() { 1171 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1172 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1173 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1174 | val specification = SpecificationsBuilder( 1175 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1176 | ).withSearch("userChildrenNumber<3").build() 1177 | val users = userRepository.findAll(specification) 1178 | Assertions.assertEquals(1, users.size) 1179 | Assertions.assertEquals("john", users[0].userFirstName) 1180 | } 1181 | 1182 | @Test 1183 | fun canGetUserWithNameIn() { 1184 | val johnId = userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)).userId 1185 | val janeId = userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)).userId 1186 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1187 | val specification = SpecificationsBuilder( 1188 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1189 | ).withSearch("userFirstName IN [\"john\", \"jane\"]").build() 1190 | val users = userRepository.findAll(specification) 1191 | Assertions.assertTrue(setOf(johnId, janeId) == users.map { user -> user.userId }.toSet()) 1192 | } 1193 | 1194 | @Test 1195 | fun canGetUserWithNameInEmptyList() { 1196 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1197 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1198 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1199 | val specification = SpecificationsBuilder( 1200 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1201 | ).withSearch("userFirstName IN []").build() 1202 | val users = userRepository.findAll(specification) 1203 | Assertions.assertTrue(users.isEmpty()) 1204 | } 1205 | 1206 | @Test 1207 | fun canGetUserWithNameNotIn() { 1208 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1209 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1210 | val joeId = userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)).userId 1211 | val specification = SpecificationsBuilder( 1212 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1213 | ).withSearch("userFirstName NOT IN [\"john\", \"jane\"]").build() 1214 | val users = userRepository.findAll(specification) 1215 | Assertions.assertTrue(setOf(joeId) == users.map { user -> user.userId }.toSet()) 1216 | } 1217 | 1218 | @Test 1219 | fun canGetUserWithChildrenNumberNotIn() { 1220 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1221 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1222 | val joeId = userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)).userId 1223 | val specification = SpecificationsBuilder( 1224 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1225 | ).withSearch("userChildrenNumber NOT IN [2, 3]").build() 1226 | val users = userRepository.findAll(specification) 1227 | Assertions.assertTrue(setOf(joeId) == users.map { user -> user.userId }.toSet()) 1228 | } 1229 | 1230 | @Test 1231 | fun canGetUserWithChildrenNumberIn() { 1232 | val johnId = userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)).userId 1233 | val janeId = userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)).userId 1234 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) 1235 | val specification = SpecificationsBuilder( 1236 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1237 | ).withSearch("userChildrenNumber IN [2, 3]").build() 1238 | val users = userRepository.findAll(specification) 1239 | Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) 1240 | } 1241 | 1242 | @Test 1243 | fun canGetUserWithTypeIn() { 1244 | val johnId = userRepository.save(Users(userFirstName = "john", type = UserType.TEAM_MEMBER)).userId 1245 | val janeId = userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)).userId 1246 | userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) 1247 | val specification = SpecificationsBuilder( 1248 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1249 | ).withSearch("type IN [ADMINISTRATOR, TEAM_MEMBER]").build() 1250 | val users = userRepository.findAll(specification) 1251 | Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) 1252 | } 1253 | 1254 | @Test 1255 | fun canGetUserWithIn() { 1256 | val johnId = 1257 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))).userId 1258 | val janeId = 1259 | userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-15"))).userId 1260 | userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2021-01-10"))) 1261 | val specification = SpecificationsBuilder( 1262 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1263 | ).withSearch("updatedDateAt IN ['2020-01-10', '2020-01-15']").build() 1264 | val users = userRepository.findAll(specification) 1265 | Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) 1266 | } 1267 | 1268 | @Test 1269 | fun canGetAuthorsWithEmptyBook() { 1270 | val johnBook = Book() 1271 | val john = Author() 1272 | john.name = "john" 1273 | john.addBook(johnBook) 1274 | authorRepository.save(john) 1275 | val janeBook = Book() 1276 | val jane = Author() 1277 | jane.name = "jane" 1278 | jane.addBook(janeBook) 1279 | authorRepository.save(jane) 1280 | val specification = SpecificationsBuilder( 1281 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1282 | ).withSearch("books IS EMPTY").build() 1283 | val users = authorRepository.findAll(specification) 1284 | Assertions.assertTrue(users.isEmpty()) 1285 | } 1286 | 1287 | @Test 1288 | fun cantSearchForEmptyWithNonFieldProperties() { 1289 | val johnBook = Book() 1290 | val john = Author() 1291 | john.name = "john" 1292 | john.addBook(johnBook) 1293 | authorRepository.save(john) 1294 | val janeBook = Book() 1295 | val jane = Author() 1296 | jane.name = "jane" 1297 | jane.addBook(janeBook) 1298 | authorRepository.save(jane) 1299 | val specification = SpecificationsBuilder( 1300 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1301 | ).withSearch("name IS EMPTY").build() 1302 | Assertions.assertThrows( 1303 | ResponseStatusException::class.java 1304 | ) { authorRepository.findAll(specification) } 1305 | val specification2 = SpecificationsBuilder( 1306 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1307 | ).withSearch("name IS NOT EMPTY").build() 1308 | Assertions.assertThrows( 1309 | ResponseStatusException::class.java 1310 | ) { authorRepository.findAll(specification2) } 1311 | } 1312 | @Test 1313 | fun canGetAuthorsWithEmptyBookWithResult() { 1314 | val johnBook = Book() 1315 | val john = Author() 1316 | john.name = "john" 1317 | john.addBook(johnBook) 1318 | authorRepository.save(john) 1319 | val jane = Author() 1320 | jane.name = "jane" 1321 | authorRepository.save(jane) 1322 | val specification = SpecificationsBuilder( 1323 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1324 | ).withSearch("books IS EMPTY").build() 1325 | val users = authorRepository.findAll(specification) 1326 | Assertions.assertTrue(users.size == 1) 1327 | Assertions.assertTrue(users[0].name == jane.name) 1328 | } 1329 | 1330 | @Test 1331 | fun canGetAuthorsWithBooksNotEmpty() { 1332 | val johnBook = Book() 1333 | val john = Author() 1334 | john.name = "john" 1335 | john.addBook(johnBook) 1336 | authorRepository.save(john) 1337 | val jane = Author() 1338 | jane.name = "jane" 1339 | authorRepository.save(jane) 1340 | val specification = SpecificationsBuilder( 1341 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1342 | ).withSearch("books IS NOT EMPTY").build() 1343 | val users = authorRepository.findAll(specification) 1344 | Assertions.assertTrue(users.size == 1) 1345 | Assertions.assertTrue(users[0].name == john.name) 1346 | } 1347 | 1348 | @Test 1349 | fun canGetAuthorsWithBooksNotEmptyAllResult() { 1350 | val johnBook = Book() 1351 | val john = Author() 1352 | john.name = "john" 1353 | john.addBook(johnBook) 1354 | authorRepository.save(john) 1355 | val jane = Author() 1356 | jane.name = "jane" 1357 | val janeBook = Book() 1358 | jane.addBook(janeBook) 1359 | authorRepository.save(jane) 1360 | val specification = SpecificationsBuilder( 1361 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1362 | ).withSearch("books IS NOT EMPTY").build() 1363 | val users = authorRepository.findAll(specification) 1364 | Assertions.assertTrue(users.size == 2) 1365 | } 1366 | 1367 | @Test 1368 | fun canGetAuthorsWithBooksNotEmptyNoResult() { 1369 | val john = Author() 1370 | john.name = "john" 1371 | authorRepository.save(john) 1372 | val jane = Author() 1373 | jane.name = "jane" 1374 | authorRepository.save(jane) 1375 | val specification = SpecificationsBuilder( 1376 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1377 | ).withSearch("books IS NOT EMPTY").build() 1378 | val users = authorRepository.findAll(specification) 1379 | Assertions.assertTrue(users.size == 0) 1380 | } 1381 | @Test 1382 | fun cantGetAuthorsWithBooksNull() { 1383 | val john = Author() 1384 | john.name = "john" 1385 | authorRepository.save(john) 1386 | val jane = Author() 1387 | jane.name = "jane" 1388 | authorRepository.save(jane) 1389 | val spec = SpecificationsBuilder( 1390 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1391 | ).withSearch("books IS NULL").build() 1392 | Assertions.assertThrows( 1393 | ResponseStatusException::class.java 1394 | ) { authorRepository.findAll(spec) } 1395 | val specNotNull = SpecificationsBuilder( 1396 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1397 | ).withSearch("books IS NOT NULL").build() 1398 | Assertions.assertThrows( 1399 | ResponseStatusException::class.java 1400 | ) { authorRepository.findAll(specNotNull) } 1401 | } 1402 | 1403 | @Test 1404 | fun canGetUsersWithNumberOfChildrenBetween() { 1405 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1406 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1407 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 5)) 1408 | userRepository.save(Users(userFirstName = "jean", userChildrenNumber = 10)) 1409 | val specification = SpecificationsBuilder( 1410 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1411 | ).withSearch("userChildrenNumber BETWEEN 4 AND 10").build() 1412 | val users = userRepository.findAll(specification) 1413 | Assertions.assertEquals(2, users.size) 1414 | val setNames = users.map { user -> user.userFirstName }.toSet() 1415 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1416 | } 1417 | 1418 | @Test 1419 | fun canGetUsersWithUpdatedDateBetween() { 1420 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 1421 | userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11"))) 1422 | userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2020-01-12"))) 1423 | userRepository.save(Users(userFirstName = "jean", updatedDateAt = LocalDate.parse("2020-01-13"))) 1424 | val specification = SpecificationsBuilder( 1425 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1426 | ).withSearch("updatedDateAt BETWEEN 2020-01-12 AND 2020-01-13").build() 1427 | val users = userRepository.findAll(specification) 1428 | Assertions.assertEquals(2, users.size) 1429 | val setNames = users.map { user -> user.userFirstName }.toSet() 1430 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1431 | } 1432 | 1433 | @Test 1434 | fun canGetUsersWithUpdatedDateNotBetween() { 1435 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 1436 | userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11"))) 1437 | userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2020-01-12"))) 1438 | userRepository.save(Users(userFirstName = "jean", updatedDateAt = LocalDate.parse("2020-01-13"))) 1439 | val specification = SpecificationsBuilder( 1440 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1441 | ).withSearch("updatedDateAt NOT BETWEEN 2020-01-12 AND 2020-01-13").build() 1442 | val users = userRepository.findAll(specification) 1443 | Assertions.assertEquals(2, users.size) 1444 | val setNames = users.map { user -> user.userFirstName }.toSet() 1445 | Assertions.assertEquals(setOf("john", "jane"), setNames) 1446 | } 1447 | 1448 | @Test 1449 | fun canGetUsersWithUpdatedDateBetweenAndIdIn() { 1450 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 1451 | userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11"))) 1452 | val joeId = userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2020-01-12"))).userId 1453 | val jeanId = userRepository.save(Users(userFirstName = "jean", updatedDateAt = LocalDate.parse("2020-01-13"))).userId 1454 | val specification = SpecificationsBuilder( 1455 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1456 | ).withSearch("updatedDateAt BETWEEN 2020-01-11 AND 2020-01-13 AND userId IN [$joeId, $jeanId]").build() 1457 | val users = userRepository.findAll(specification) 1458 | Assertions.assertEquals(2, users.size) 1459 | val setNames = users.map { user -> user.userFirstName }.toSet() 1460 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1461 | } 1462 | 1463 | @Test 1464 | fun canGetUsersWithUserFirstNameBetween() { 1465 | userRepository.save(Users(userFirstName = "abel")) 1466 | userRepository.save(Users(userFirstName = "bob")) 1467 | userRepository.save(Users(userFirstName = "connor")) 1468 | userRepository.save(Users(userFirstName = "david")) 1469 | val specification = SpecificationsBuilder( 1470 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1471 | ).withSearch("userFirstName BETWEEN 'aaron' AND 'cyrano'").build() 1472 | val users = userRepository.findAll(specification) 1473 | Assertions.assertEquals(3, users.size) 1474 | val setNames = users.map { user -> user.userFirstName }.toSet() 1475 | Assertions.assertEquals(setOf("abel", "bob", "connor"), setNames) 1476 | } 1477 | @Test 1478 | fun canGetUsersWithUserFirstNameNotBetween() { 1479 | userRepository.save(Users(userFirstName = "abel")) 1480 | userRepository.save(Users(userFirstName = "bob")) 1481 | userRepository.save(Users(userFirstName = "connor")) 1482 | userRepository.save(Users(userFirstName = "david")) 1483 | val specification = SpecificationsBuilder( 1484 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1485 | ).withSearch("userFirstName NOT BETWEEN 'aaron' AND 'cyrano'").build() 1486 | val users = userRepository.findAll(specification) 1487 | Assertions.assertEquals(1, users.size) 1488 | val setNames = users.map { user -> user.userFirstName }.toSet() 1489 | Assertions.assertEquals(setOf("david"), setNames) 1490 | } 1491 | @Test 1492 | fun canGetUsersWithUserFirstNameGt() { 1493 | userRepository.save(Users(userFirstName = "abel")) 1494 | userRepository.save(Users(userFirstName = "bob")) 1495 | userRepository.save(Users(userFirstName = "connor")) 1496 | userRepository.save(Users(userFirstName = "david")) 1497 | val specification = SpecificationsBuilder( 1498 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1499 | ).withSearch("userFirstName > barry'").build() 1500 | val users = userRepository.findAll(specification) 1501 | Assertions.assertEquals(3, users.size) 1502 | val setNames = users.map { user -> user.userFirstName }.toSet() 1503 | Assertions.assertEquals(setOf("connor", "david", "bob"), setNames) 1504 | } 1505 | 1506 | @Test 1507 | fun canGetUsersWithUserFirstNameLt() { 1508 | userRepository.save(Users(userFirstName = "abel")) 1509 | userRepository.save(Users(userFirstName = "bob")) 1510 | userRepository.save(Users(userFirstName = "connor")) 1511 | userRepository.save(Users(userFirstName = "david")) 1512 | val specification = SpecificationsBuilder( 1513 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1514 | ).withSearch("userFirstName < barry'").build() 1515 | val users = userRepository.findAll(specification) 1516 | Assertions.assertEquals(1, users.size) 1517 | val setNames = users.map { user -> user.userFirstName }.toSet() 1518 | Assertions.assertEquals(setOf("abel"), setNames) 1519 | } 1520 | 1521 | @Test 1522 | fun canGetUsersWithUserFirstNameCaseSensitive() { 1523 | userRepository.save(Users(userFirstName = "abel")) 1524 | userRepository.save(Users(userFirstName = "Aaron")) 1525 | userRepository.save(Users(userFirstName = "connor")) 1526 | userRepository.save(Users(userFirstName = "david")) 1527 | // create spec with case sensitive flag 1528 | val specification = SpecificationsBuilder( 1529 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1530 | ).withSearch("userFirstName : A*").build() 1531 | val users = userRepository.findAll(specification) 1532 | Assertions.assertEquals(1, users.size) 1533 | Assertions.assertEquals("Aaron", users[0].userFirstName) 1534 | } 1535 | // test for a wrong search, should throw an exception during the parse 1536 | @Test 1537 | fun badRequestWithWrongSearch() { 1538 | Assertions.assertThrows(ResponseStatusException::class.java) { 1539 | SpecificationsBuilder( 1540 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1541 | ).withSearch("userFirstName : ").build() 1542 | } 1543 | Assertions.assertThrows(ResponseStatusException::class.java) { 1544 | SpecificationsBuilder( 1545 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1546 | ).withSearch("updatedDateAt BETWEEN AND 2020-01-11").build() 1547 | } 1548 | Assertions.assertThrows(ResponseStatusException::class.java) { 1549 | SpecificationsBuilder( 1550 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1551 | ).withSearch("updatedDateAt BETWEEN 2020-01-11 AND").build() 1552 | } 1553 | Assertions.assertThrows(ResponseStatusException::class.java) { 1554 | SpecificationsBuilder( 1555 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1556 | ).withSearch("books IS EMPT ").build() 1557 | } 1558 | Assertions.assertThrows(ResponseStatusException::class.java) { 1559 | SpecificationsBuilder( 1560 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1561 | ).withSearch("books IS NOT EMPT ").build() 1562 | } 1563 | Assertions.assertThrows(ResponseStatusException::class.java) { 1564 | SpecificationsBuilder( 1565 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1566 | ).withSearch("userId IN [").build() 1567 | } 1568 | } 1569 | 1570 | @Test 1571 | fun canGetUsersWithNullColumn() { 1572 | userRepository.save(Users(userFirstName = "john", type = null)) 1573 | userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) 1574 | userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) 1575 | userRepository.save(Users(userFirstName = "jean", type = null)) 1576 | val specification = SpecificationsBuilder( 1577 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1578 | ).withSearch("type IS NULL").build() 1579 | val users = userRepository.findAll(specification) 1580 | Assertions.assertEquals(2, users.size) 1581 | val setNames = users.map { user -> user.userFirstName }.toSet() 1582 | Assertions.assertEquals(setOf("john", "jean"), setNames) 1583 | } 1584 | 1585 | @Test 1586 | fun canGetUsersWithNotNullColumn() { 1587 | userRepository.save(Users(userFirstName = "john", type = null)) 1588 | userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) 1589 | userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) 1590 | userRepository.save(Users(userFirstName = "jean", type = null)) 1591 | val specification = SpecificationsBuilder( 1592 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1593 | ).withSearch("type IS NOT NULL").build() 1594 | val users = userRepository.findAll(specification) 1595 | Assertions.assertEquals(2, users.size) 1596 | val setNames = users.map { user -> user.userFirstName }.toSet() 1597 | Assertions.assertEquals(setOf("jane", "joe"), setNames) 1598 | } 1599 | 1600 | @Test 1601 | fun canGetUsersWithNotNullFirstName() { 1602 | userRepository.save(Users(userFirstName = "john", type = null)) 1603 | userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)) 1604 | userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) 1605 | userRepository.save(Users(userFirstName = "jean", type = null)) 1606 | val specification = SpecificationsBuilder( 1607 | SearchSpec::class.constructors.first().call("", true, emptyArray()) 1608 | ).withSearch("userFirstName IS NOT NULL").build() 1609 | val users = userRepository.findAll(specification) 1610 | Assertions.assertEquals(4, users.size) 1611 | val setNames = users.map { user -> user.userFirstName }.toSet() 1612 | Assertions.assertEquals(setOf("john", "jane", "joe", "jean"), setNames) 1613 | } 1614 | 1615 | @Test 1616 | fun canGetUserWithNullSalary() { 1617 | userRepository.save(Users(userFirstName = "john", userSalary = 100.0F)) 1618 | userRepository.save(Users(userFirstName = "jane", userSalary = 1000.0F)) 1619 | val specification = SpecificationsBuilder( 1620 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1621 | ).withSearch("userSalary IS NULL").build() 1622 | val users = userRepository.findAll(specification) 1623 | Assertions.assertEquals(0, users.size) 1624 | } 1625 | 1626 | @Test 1627 | fun canNotSearchABlackListedField() { 1628 | Assertions.assertThrows(ResponseStatusException::class.java) { 1629 | SpecificationsBuilder( 1630 | SearchSpec::class.constructors.first().call("", false, arrayOf("userFirstName")) 1631 | ).withSearch("userFirstName : A* AND userId : 3").build() 1632 | } 1633 | } 1634 | 1635 | @Test 1636 | fun canGetUsersWithUUIDNull() { 1637 | val userUUID = UUID.randomUUID() 1638 | userRepository.save(Users(userFirstName = "Diego", uuid = userUUID)) 1639 | userRepository.save(Users(userFirstName = "Diego two", uuid = null)) 1640 | val specification = SpecificationsBuilder( 1641 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1642 | ).withSearch("uuid IS NULL").build() 1643 | val robotUsers = userRepository.findAll(specification) 1644 | Assertions.assertEquals(1, robotUsers.size) 1645 | Assertions.assertEquals(null, robotUsers[0].uuid) 1646 | } 1647 | 1648 | @Test 1649 | fun canGetUsersWithUpdatedDateAtNull() { 1650 | userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) 1651 | userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11"))) 1652 | userRepository.save(Users(userFirstName = "joe", updatedDateAt = null)) 1653 | userRepository.save(Users(userFirstName = "jean", updatedDateAt = null)) 1654 | val specification = SpecificationsBuilder( 1655 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1656 | ).withSearch("updatedDateAt IS NULL").build() 1657 | val users = userRepository.findAll(specification) 1658 | Assertions.assertEquals(2, users.size) 1659 | val setNames = users.map { user -> user.userFirstName }.toSet() 1660 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1661 | } 1662 | 1663 | @Test 1664 | fun canGetUsersWithUpdatedDateTimeAtNotNull() { 1665 | userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) 1666 | userRepository.save(Users(userFirstName = "jane", updatedAt = LocalDateTime.parse("2020-01-11T10:15:30"))) 1667 | userRepository.save(Users(userFirstName = "joe", updatedAt = null)) 1668 | userRepository.save(Users(userFirstName = "jean", updatedAt = LocalDateTime.parse("2020-01-13T10:15:30"))) 1669 | val specification = SpecificationsBuilder( 1670 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1671 | ).withSearch("updatedAt IS NOT NULL").build() 1672 | val users = userRepository.findAll(specification) 1673 | Assertions.assertEquals(3, users.size) 1674 | val setNames = users.map { user -> user.userFirstName }.toSet() 1675 | Assertions.assertEquals(setOf("john", "jane", "jean"), setNames) 1676 | } 1677 | @Test 1678 | fun canGetUsersWithActiveNull() { 1679 | userRepository.save(Users(userFirstName = "john", active = true)) 1680 | userRepository.save(Users(userFirstName = "jane", active = false)) 1681 | userRepository.save(Users(userFirstName = "joe", active = null)) 1682 | userRepository.save(Users(userFirstName = "jean", active = null)) 1683 | val specification = SpecificationsBuilder( 1684 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1685 | ).withSearch("active IS NULL").build() 1686 | val users = userRepository.findAll(specification) 1687 | Assertions.assertEquals(2, users.size) 1688 | val setNames = users.map { user -> user.userFirstName }.toSet() 1689 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1690 | } 1691 | @Test 1692 | fun canGetUsersWithUserChildrenNumberNull() { 1693 | userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) 1694 | userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) 1695 | userRepository.save(Users(userFirstName = "joe", userChildrenNumber = null)) 1696 | userRepository.save(Users(userFirstName = "jean", userChildrenNumber = null)) 1697 | val specification = SpecificationsBuilder( 1698 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1699 | ).withSearch("userChildrenNumber IS NULL").build() 1700 | val users = userRepository.findAll(specification) 1701 | Assertions.assertEquals(2, users.size) 1702 | val setNames = users.map { user -> user.userFirstName }.toSet() 1703 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1704 | } 1705 | @Test 1706 | fun canGetUsersWithCreatedAtNull() { 1707 | userRepository.save(Users(userFirstName = "john", createdAt = Date())) 1708 | userRepository.save(Users(userFirstName = "jane", createdAt = Date())) 1709 | userRepository.save(Users(userFirstName = "joe", createdAt = null)) 1710 | userRepository.save(Users(userFirstName = "jean", createdAt = null)) 1711 | val specification = SpecificationsBuilder( 1712 | SearchSpec::class.constructors.first().call("", false, emptyArray()) 1713 | ).withSearch("createdAt IS NULL").build() 1714 | val users = userRepository.findAll(specification) 1715 | Assertions.assertEquals(2, users.size) 1716 | val setNames = users.map { user -> user.userFirstName }.toSet() 1717 | Assertions.assertEquals(setOf("joe", "jean"), setNames) 1718 | } 1719 | } 1720 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/UserType.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | enum class UserType { 4 | ADMINISTRATOR, 5 | MANAGER, 6 | TEAM_MEMBER 7 | } 8 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/Users.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import jakarta.persistence.Column 4 | import jakarta.persistence.Entity 5 | import jakarta.persistence.GeneratedValue 6 | import jakarta.persistence.GenerationType 7 | import jakarta.persistence.Id 8 | import jakarta.persistence.Table 9 | import java.time.Duration 10 | import java.time.Instant 11 | import java.time.LocalDate 12 | import java.time.LocalDateTime 13 | import java.time.LocalTime 14 | import java.util.Date 15 | import java.util.UUID 16 | 17 | @Entity 18 | @Table(name = "USERS") 19 | data class Users( 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.AUTO) 22 | var userId: Int? = null, 23 | 24 | @Column(name = "FirstName") 25 | var userFirstName: String = "John", 26 | 27 | @Column(name = "isAdmin") 28 | var isAdmin: Boolean = true, 29 | 30 | @Column(name = "LastName") 31 | var userLastName: String = "Doe", 32 | 33 | @Column(name = "email") 34 | var userEmail: String = "john.doe@wanahoo.fr", 35 | 36 | @Column(name = "PostalAddress") 37 | var userAddress: String = "1 rue de l'angleterre", 38 | 39 | @Column(name = "NumberOfChildren") 40 | var userChildrenNumber: Int? = 3, 41 | 42 | @Column(name = "Salary") 43 | var userSalary: Float = 3000.0F, 44 | 45 | @Column(name = "AgeInSeconds") 46 | var userAgeInSeconds: Double = 1261440000.0, 47 | 48 | @Column 49 | var createdAt: Date? = Date(), 50 | 51 | @Column 52 | var updatedAt: LocalDateTime? = LocalDateTime.now(), 53 | 54 | @Column 55 | var updatedTimeAt: LocalTime = LocalTime.now(), 56 | 57 | @Column 58 | var updatedDateAt: LocalDate? = LocalDate.now(), 59 | 60 | @Column 61 | var updatedInstantAt: Instant = Instant.now(), 62 | 63 | @Column 64 | var validityDuration: Duration = Duration.ofDays(30), 65 | 66 | @Column(name = "UserType") 67 | var type: UserType? = UserType.TEAM_MEMBER, 68 | 69 | @Column 70 | var uuid: UUID? = UUID.randomUUID(), 71 | 72 | @Column 73 | var active: Boolean? = true 74 | ) 75 | -------------------------------------------------------------------------------- /src/test/kotlin/com/sipios/springsearch/UsersRepository.kt: -------------------------------------------------------------------------------- 1 | package com.sipios.springsearch 2 | 3 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource 6 | 7 | @RepositoryRestResource(path = "users") 8 | interface UsersRepository : CrudRepository , JpaSpecificationExecutor 9 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:testdb 2 | spring.datasource.driver-class-name=org.h2.Driver 3 | spring.datasource.username=sa 4 | --------------------------------------------------------------------------------