├── .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 |
5 |
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 |
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 | 
159 | 2. Using space in nouns
160 | Request : `/cars?search=model:'Spacetourer Business Lounge'`
161 | 
162 | 3. Using special characters
163 | Request: `/cars?search=model:中华V7`
164 | 
165 | 4. Using deep fields
166 | Request : `/cars?search=options.transmission:Auto`
167 | 
168 | 5. Complex example
169 | Request : `/cars?search=creationyear:2018 AND price<300000 AND (color:Yellow OR color:Blue) AND options.transmission:Auto`
170 | 
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