Repository: spring-projects/spring-data-keyvalue Branch: main Commit: 5c38162264cb Files: 158 Total size: 462.9 KB Directory structure: gitextract_fy98wr_9/ ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ ├── README.template.adoc │ ├── dco.yml │ └── workflows/ │ ├── ci.yml │ ├── codeql.yml │ ├── project.yml │ └── snapshots.yml ├── .gitignore ├── .mvn/ │ ├── extensions.xml │ ├── jvm.config │ └── wrapper/ │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CI.adoc ├── CONTRIBUTING.adoc ├── LICENSE.txt ├── README.adoc ├── SECURITY.adoc ├── mvnw ├── mvnw.cmd ├── package.json ├── pom.xml ├── settings.xml └── src/ ├── main/ │ ├── antora/ │ │ ├── antora-playbook.yml │ │ ├── antora.yml │ │ ├── modules/ │ │ │ └── ROOT/ │ │ │ ├── nav.adoc │ │ │ └── pages/ │ │ │ ├── commons/ │ │ │ │ └── upgrade.adoc │ │ │ ├── index.adoc │ │ │ ├── keyvalue/ │ │ │ │ ├── repository/ │ │ │ │ │ └── map-repositories.adoc │ │ │ │ ├── template.adoc │ │ │ │ └── value-expressions.adoc │ │ │ ├── keyvalue.adoc │ │ │ ├── repositories/ │ │ │ │ ├── core-concepts.adoc │ │ │ │ ├── core-domain-events.adoc │ │ │ │ ├── core-extensions.adoc │ │ │ │ ├── create-instances.adoc │ │ │ │ ├── custom-implementations.adoc │ │ │ │ ├── definition.adoc │ │ │ │ ├── null-handling.adoc │ │ │ │ ├── object-mapping.adoc │ │ │ │ ├── projections.adoc │ │ │ │ ├── query-keywords-reference.adoc │ │ │ │ ├── query-methods-details.adoc │ │ │ │ └── query-return-types-reference.adoc │ │ │ └── repositories.adoc │ │ └── resources/ │ │ └── antora-resources/ │ │ └── antora.yml │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── data/ │ │ ├── keyvalue/ │ │ │ ├── annotation/ │ │ │ │ ├── KeySpace.java │ │ │ │ └── package-info.java │ │ │ ├── aot/ │ │ │ │ ├── KeyValueRuntimeHints.java │ │ │ │ └── package-info.java │ │ │ ├── core/ │ │ │ │ ├── AbstractKeyValueAdapter.java │ │ │ │ ├── CriteriaAccessor.java │ │ │ │ ├── DefaultIdentifierGenerator.java │ │ │ │ ├── ForwardingCloseableIterator.java │ │ │ │ ├── GeneratingIdAccessor.java │ │ │ │ ├── IdentifierGenerator.java │ │ │ │ ├── IterableConverter.java │ │ │ │ ├── KeyValueAdapter.java │ │ │ │ ├── KeyValueCallback.java │ │ │ │ ├── KeyValueOperations.java │ │ │ │ ├── KeyValuePersistenceExceptionTranslator.java │ │ │ │ ├── KeyValueTemplate.java │ │ │ │ ├── PathSortAccessor.java │ │ │ │ ├── PredicateQueryEngine.java │ │ │ │ ├── PropertyPathComparator.java │ │ │ │ ├── QueryEngine.java │ │ │ │ ├── QueryEngineFactory.java │ │ │ │ ├── SimplePropertyPathAccessor.java │ │ │ │ ├── SortAccessor.java │ │ │ │ ├── SpelCriteria.java │ │ │ │ ├── SpelCriteriaAccessor.java │ │ │ │ ├── SpelPropertyComparator.java │ │ │ │ ├── SpelQueryEngine.java │ │ │ │ ├── SpelSortAccessor.java │ │ │ │ ├── UncategorizedKeyValueException.java │ │ │ │ ├── event/ │ │ │ │ │ ├── KeyValueEvent.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── mapping/ │ │ │ │ │ ├── AnnotationBasedKeySpaceResolver.java │ │ │ │ │ ├── BasicKeyValuePersistentEntity.java │ │ │ │ │ ├── ClassNameKeySpaceResolver.java │ │ │ │ │ ├── KeySpaceResolver.java │ │ │ │ │ ├── KeyValuePersistentEntity.java │ │ │ │ │ ├── KeyValuePersistentProperty.java │ │ │ │ │ ├── PrefixKeyspaceResolver.java │ │ │ │ │ ├── context/ │ │ │ │ │ │ ├── KeyValueMappingContext.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── query/ │ │ │ │ ├── KeyValueQuery.java │ │ │ │ └── package-info.java │ │ │ └── repository/ │ │ │ ├── KeyValueRepository.java │ │ │ ├── config/ │ │ │ │ ├── KeyValueRepositoryConfigurationExtension.java │ │ │ │ ├── QueryCreatorType.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── query/ │ │ │ │ ├── CachingKeyValuePartTreeQuery.java │ │ │ │ ├── KeyValuePartTreeQuery.java │ │ │ │ ├── PredicateQueryCreator.java │ │ │ │ ├── SpelQueryCreator.java │ │ │ │ └── package-info.java │ │ │ └── support/ │ │ │ ├── KeyValueQuerydslUtils.java │ │ │ ├── KeyValueRepositoryFactory.java │ │ │ ├── KeyValueRepositoryFactoryBean.java │ │ │ ├── QuerydslKeyValuePredicateExecutor.java │ │ │ ├── QuerydslKeyValueRepository.java │ │ │ ├── SimpleKeyValueRepository.java │ │ │ └── package-info.java │ │ └── map/ │ │ ├── KeySpaceStore.java │ │ ├── MapKeySpaceStore.java │ │ ├── MapKeyValueAdapter.java │ │ ├── package-info.java │ │ └── repository/ │ │ └── config/ │ │ ├── EnableMapRepositories.java │ │ ├── MapRepositoriesRegistrar.java │ │ ├── MapRepositoryConfigurationExtension.java │ │ └── package-info.java │ └── resources/ │ ├── META-INF/ │ │ ├── spring/ │ │ │ └── aot.factories │ │ └── spring.factories │ ├── license.txt │ └── notice.txt └── test/ ├── java/ │ └── org/ │ └── springframework/ │ └── data/ │ ├── keyvalue/ │ │ ├── CustomKeySpaceAnnotationWithAliasFor.java │ │ ├── Person.java │ │ ├── SubclassOfTypeWithCustomComposedKeySpaceAnnotation.java │ │ ├── TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor.java │ │ ├── TypeWithDirectKeySpaceAnnotation.java │ │ ├── TypeWithInhteritedPersistentAnnotationNotHavingKeySpace.java │ │ ├── TypeWithPersistentAnnotationNotHavingKeySpace.java │ │ ├── core/ │ │ │ ├── DefaultIdentifierGeneratorUnitTests.java │ │ │ ├── ForwardingCloseableIteratorUnitTests.java │ │ │ ├── IterableConverterUnitTests.java │ │ │ ├── KeyValuePersistenceExceptionTranslatorUnitTests.java │ │ │ ├── KeyValueTemplateTests.java │ │ │ ├── KeyValueTemplateUnitTests.java │ │ │ ├── PredicateQueryEngineUnitTests.java │ │ │ ├── PropertyPathComparatorUnitTests.java │ │ │ ├── SpelPropertyComparatorUnitTests.java │ │ │ ├── SpelQueryEngineUnitTests.java │ │ │ └── mapping/ │ │ │ ├── AnnotationBasedKeySpaceResolverUnitTests.java │ │ │ ├── BasicKeyValuePersistentEntityUnitTests.java │ │ │ ├── PrefixKeyspaceResolverUnitTests.java │ │ │ └── context/ │ │ │ └── KeyValueMappingContextUnitTests.java │ │ └── repository/ │ │ ├── MapRepositoriesRegistrarUnitTests.java │ │ ├── SimpleKeyValueRepositoryUnitTests.java │ │ ├── query/ │ │ │ ├── AbstractQueryCreatorTestBase.java │ │ │ ├── CachingKeyValuePartTreeQueryUnitTests.java │ │ │ ├── KeyValuePartTreeQueryUnitTests.java │ │ │ ├── PredicateQueryCreatorUnitTests.java │ │ │ └── SpelQueryCreatorUnitTests.java │ │ └── support/ │ │ ├── KeyValueQuerydslUtilsUnitTests.java │ │ └── KeyValueRepositoryFactoryBeanUnitTests.java │ └── map/ │ ├── AbstractRepositoryUnitTests.java │ ├── CachingQuerySimpleKeyValueRepositoryUnitTests.java │ ├── MapDbIntegrationTests.java │ ├── MapKeyValueAdapterUnitTests.java │ ├── QuerydslKeyValuePredicateExecutorUnitTests.java │ ├── SimpleKeyValueRepositoryUnitTests.java │ └── repository/ │ └── config/ │ ├── MapRepositoriesConfigurationExtensionIntegrationTests.java │ ├── MapRepositoryRegistrarWithFullDefaultingIntegrationTests.java │ └── MapRepositoryRegistrarWithTemplateDefinitionIntegrationTests.java └── resources/ └── logback.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ - [ ] You have read the [Spring Data contribution guidelines](https://github.com/spring-projects/spring-data-build/blob/master/CONTRIBUTING.adoc). - [ ] You use the code formatters provided [here](https://github.com/spring-projects/spring-data-build/tree/master/etc/ide) and have them applied to your changes. Don’t submit any formatting related changes. - [ ] You submit test cases (unit or integration tests) that back your changes. - [ ] You added yourself as author in the headers of the classes you touched. Amend the date range in the Apache license header if needed. For new types, add the license header (copy from another file and set the current year only). ================================================ FILE: .github/README.template.adoc ================================================ = Spring Data KeyValue image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data KeyValue"] The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services. This module provides infrastructure components to build repository abstractions for stores dealing with Key/Value pairs and ships with a default `java.util.Map` based implementation. == Features * Infrastructure for building repositories on top of key/value implementations. * Dynamic SpEL query generation from query method names. * Possibility to integrate custom repository code. == Code of Conduct This project is governed by the https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[Spring Code of Conduct]. By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. == Getting Started Here is a quick teaser of an application using Spring Data Repositories in Java: [source,java] ---- public interface PersonRepository extends CrudRepository { List findByLastname(String lastname); List findByFirstnameLike(String firstname); } @Service public class MyService { private final PersonRepository repository; public MyService(PersonRepository repository) { this.repository = repository; } public void doWork() { repository.deleteAll(); Person person = new Person(); person.setFirstname("Oliver"); person.setLastname("Gierke"); repository.save(person); List lastNameResults = repository.findByLastname("Gierke"); List firstNameResults = repository.findByFirstnameLike("Oli*"); } } @KeySpace("person") class Person { @Id String uuid; String firstname; String lastname; // getters and setters omitted for brevity } @Configuration @EnableMapRepositories("com.acme.repositories") class AppConfig { … } ---- === Maven configuration Add the Maven dependency: [source,xml] ---- org.springframework.data spring-data-keyvalue ${version}.RELEASE ---- If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version. [source,xml] ---- org.springframework.data spring-data-keyvalue ${version}-SNAPSHOT spring-snapshot Spring Snapshot Repository https://repo.spring.io/snapshot ---- == Getting Help Having trouble with Spring Data? We’d love to help! * Check the https://docs.spring.io/spring-data/keyvalue/reference/[reference documentation], and https://docs.spring.io/spring-data/keyvalue/docs/current/api/[Javadocs]. * Learn the Spring basics – Spring Data builds on Spring Framework, check the https://spring.io[spring.io] web-site for a wealth of reference documentation. If you are just starting out with Spring, try one of the https://spring.io/guides[guides]. * If you are upgrading, check out the https://github.com/spring-projects/spring-data-commons/wiki#release-notes[release notes] for "`new and noteworthy`" features. * Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-data[`spring-data-keyvalue`]. * Report bugs with Spring Data KeyValue via https://github.com/spring-projects/spring-data-keyvalue/issues[Github]. == Reporting Issues Spring Data uses Github as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below: * Before you log a bug, please search the https://github.com/spring-projects/spring-data-keyvalue/issues[issue tracker] to see if someone has already reported the problem. * If the issue does not already exist, https://github.com/spring-projects/spring-data-keyvalue/issues/new[create a new issue]. * Please provide as much information as possible with the issue report, we like to know the version of Spring Data that you are using, the JVM version, Stacktrace, etc. * If you need to paste code, or include a stack trace use https://guides.github.com/features/mastering-markdown/[Markdown] code fences +++```+++. * If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. == Building from Source You don’t need to build from source to use Spring Data (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Data can be easily built with the https://github.com/takari/maven-wrapper[maven wrapper]. You also need JDK 17. [source,bash] ---- $ ./mvnw clean install ---- If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.5.0 or above]. _Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please sign the https://cla.pivotal.io/sign/spring[Contributor’s Agreement] before your first change, is trivial._ === Building reference documentation Building the documentation builds also the project without running tests. [source,bash] ---- $ ./mvnw clean install -Pantora ---- The generated documentation is available from `target/antora/site/index.html`. == Examples * https://github.com/spring-projects/spring-data-examples/[Spring Data Examples] contains example projects that explain specific features in more detail. == License Spring Data KeyValue is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license]. ================================================ FILE: .github/dco.yml ================================================ require: members: false ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI Build on: workflow_dispatch: push: branches: [ main, 'issue/**' ] permissions: read-all jobs: build-java: strategy: matrix: java-version: [ base, main ] name: Build project runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup Java and Maven uses: spring-projects/spring-data-build/actions/setup-maven@main with: java-version: ${{ matrix.java-version }} develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}' - name: Build uses: spring-projects/spring-data-build/actions/maven-build@main ================================================ FILE: .github/workflows/codeql.yml ================================================ # GitHub Actions for CodeQL Scanning name: "CodeQL Advanced" on: push: pull_request: workflow_dispatch: schedule: # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule - cron: '0 5 * * *' permissions: read-all jobs: codeql-analysis-call: permissions: actions: read contents: read security-events: write uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1 ================================================ FILE: .github/workflows/project.yml ================================================ # GitHub Actions to automate GitHub issues for Spring Data Project Management name: Spring Data GitHub Issues on: issues: types: [opened, edited, reopened] issue_comment: types: [created] pull_request_target: types: [opened, edited, reopened] permissions: contents: read issues: write pull-requests: write jobs: Inbox: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null && !contains(join(github.event.issue.labels.*.name, ', '), 'dependency-upgrade') && !contains(github.event.issue.title, 'Release ') steps: - name: Create or Update Issue Card uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/spring-projects/projects/25 github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} Pull-Request: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request != null steps: - name: Create or Update Pull Request Card uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/spring-projects/projects/25 github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} Feedback-Provided: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.actor != 'spring-projects-issues' && github.event.pull_request == null && github.event.issue.state == 'open' && contains(toJSON(github.event.issue.labels), 'waiting-for-feedback') steps: - name: Update Project Card uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/spring-projects/projects/25 github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} ================================================ FILE: .github/workflows/snapshots.yml ================================================ name: Snapshots on: workflow_dispatch: push: branches: [ main, 'issue/**' ] permissions: read-all jobs: build-snapshots: name: Build and deploy snapshots if: ${{ github.repository_owner == 'spring-projects' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup Java and Maven uses: spring-projects/spring-data-build/actions/setup-maven@main with: develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}' - name: Deploy to Artifactory uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main with: build-name: 'spring-data-keyvalue' username: '${{ secrets.ARTIFACTORY_USERNAME }}' password: '${{ secrets.ARTIFACTORY_PASSWORD }}' ================================================ FILE: .gitignore ================================================ target/ .settings/ .project .classpath .springBeans .DS_Store *.iml *.ipr *.iws /.idea/ node_modules node package-lock.json build/ .mvn/.develocity ================================================ FILE: .mvn/extensions.xml ================================================ io.spring.develocity.conventions develocity-conventions-maven-extension 0.0.25 ================================================ FILE: .mvn/jvm.config ================================================ --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ #Thu Jul 17 13:59:50 CEST 2025 distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip ================================================ FILE: CI.adoc ================================================ = Continuous Integration == Running CI tasks locally You can run CI jobs locally using Docker and act[https://nektosact.com/]. ================================================ FILE: CONTRIBUTING.adoc ================================================ = Spring Data contribution guidelines You find the contribution guidelines for Spring Data projects https://github.com/spring-projects/spring-data-build/blob/main/CONTRIBUTING.adoc[here]. ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.adoc ================================================ = Spring Data KeyValue image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data KeyValue"] The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services. This module provides infrastructure components to build repository abstractions for stores dealing with Key/Value pairs and ships with a default `java.util.Map` based implementation. == Features * Infrastructure for building repositories on top of key/value implementations. * Dynamic SpEL query generation from query method names. * Possibility to integrate custom repository code. include::https://raw.githubusercontent.com/spring-projects/spring-data-build/refs/heads/main/etc/readme/code-of-conduct.adoc[] == Getting Started Here is a quick teaser of an application using Spring Data Repositories in Java: [source,java] ---- public interface PersonRepository extends CrudRepository { List findByLastname(String lastname); List findByFirstnameLike(String firstname); } @Service public class MyService { private final PersonRepository repository; public MyService(PersonRepository repository) { this.repository = repository; } public void doWork() { repository.deleteAll(); Person person = new Person(); person.setFirstname("Oliver"); person.setLastname("Gierke"); repository.save(person); List lastNameResults = repository.findByLastname("Gierke"); List firstNameResults = repository.findByFirstnameLike("Oli*"); } } @KeySpace("person") class Person { @Id String uuid; String firstname; String lastname; // getters and setters omitted for brevity } @Configuration @EnableMapRepositories("com.acme.repositories") class AppConfig { … } ---- include::https://raw.githubusercontent.com/spring-projects/spring-data-build/refs/heads/main/etc/readme/dependencies.adoc[] include::https://raw.githubusercontent.com/spring-projects/spring-data-build/refs/heads/main/etc/readme/getting-help.adoc[] include::https://raw.githubusercontent.com/spring-projects/spring-data-build/refs/heads/main/etc/readme/license.adoc[] ================================================ FILE: SECURITY.adoc ================================================ = Security Policy == Reporting a Vulnerability Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team. Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions]. For more details, check out our https://spring.io/security-policy[security policy]. == JAR signing Spring Data JARs released on Maven Central are signed. You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt Versions released prior to 2023 may be signed with a different key. ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi wget "$jarUrl" -O "$wrapperJarPath" elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi curl -o "$wrapperJarPath" "$jarUrl" else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven2 Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( echo Found %WRAPPER_JAR% ) else ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" echo Finished downloading %WRAPPER_JAR% ) @REM End of extension %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: package.json ================================================ { "dependencies": { "antora": "3.2.0-alpha.6", "@antora/atlas-extension": "1.0.0-alpha.2", "@antora/collector-extension": "1.0.0-alpha.7", "@asciidoctor/tabs": "1.0.0-beta.6", "@springio/antora-extensions": "1.13.0", "@springio/asciidoctor-extensions": "1.0.0-alpha.11" } } ================================================ FILE: pom.xml ================================================ 4.0.0 org.springframework.data spring-data-keyvalue 4.1.0-SNAPSHOT Spring Data KeyValue org.springframework.data.build spring-data-parent 4.1.0-SNAPSHOT 4.1.0-SNAPSHOT 3.1.0 spring.data.keyvalue org.springframework.data spring-data-commons ${springdata.commons} org.springframework spring-context org.springframework spring-tx com.querydsl querydsl-collections ${querydsl} true org.mapdb mapdb ${mapdb} test com.mysema.maven apt-maven-plugin ${apt} generate-test-sources test-process target/generated-test-sources com.querydsl.apt.QuerydslAnnotationProcessor org.apache.maven.plugins maven-assembly-plugin none antora-process-resources src/main/antora/resources/antora-resources true antora org.antora antora-maven-plugin spring-snapshot https://repo.spring.io/snapshot true false spring-milestone https://repo.spring.io/milestone ================================================ FILE: settings.xml ================================================ spring-plugins-release ${env.ARTIFACTORY_USR} ${env.ARTIFACTORY_PSW} spring-libs-snapshot ${env.ARTIFACTORY_USR} ${env.ARTIFACTORY_PSW} spring-libs-milestone ${env.ARTIFACTORY_USR} ${env.ARTIFACTORY_PSW} spring-libs-release ${env.ARTIFACTORY_USR} ${env.ARTIFACTORY_PSW} ================================================ FILE: src/main/antora/antora-playbook.yml ================================================ # PACKAGES antora@3.2.0-alpha.2 @antora/atlas-extension:1.0.0-alpha.1 @antora/collector-extension@1.0.0-alpha.3 @springio/antora-extensions@1.1.0-alpha.2 @asciidoctor/tabs@1.0.0-alpha.12 @opendevise/antora-release-line-extension@1.0.0-alpha.2 # # The purpose of this Antora playbook is to build the docs in the current branch. antora: extensions: - require: '@springio/antora-extensions' root_component_name: 'data-keyvalue' site: title: Spring Data KeyValue url: https://docs.spring.io/spring-data/keyvalue/reference/ content: sources: - url: ./../../.. branches: HEAD start_path: src/main/antora worktrees: true - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ branches: [ main ] start_path: src/main/antora asciidoc: attributes: hide-uri-scheme: '@' tabs-sync-option: '@' extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' - '@springio/asciidoctor-extensions/javadoc-extension' sourcemap: true urls: latest_version_segment: '' runtime: log: failure_level: warn format: pretty ui: bundle: url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip snapshot: true ================================================ FILE: src/main/antora/antora.yml ================================================ name: data-keyvalue version: true title: Spring Data KeyValue nav: - modules/ROOT/nav.adoc ext: collector: - run: command: ./mvnw validate process-resources dependency:unpack -am -Pantora-process-resources local: true scan: - dir: target/classes/ - dir: target/antora/ ================================================ FILE: src/main/antora/modules/ROOT/nav.adoc ================================================ * xref:index.adoc[Overview] * xref:keyvalue.adoc[] * xref:keyvalue/template.adoc[] * xref:repositories.adoc[] ** xref:repositories/core-concepts.adoc[] ** xref:repositories/definition.adoc[] ** xref:repositories/create-instances.adoc[] ** xref:keyvalue/repository/map-repositories.adoc[] ** xref:keyvalue/value-expressions.adoc[] ** xref:repositories/query-keywords-reference.adoc[] ** xref:repositories/query-return-types-reference.adoc[] * xref:attachment$api/java/index.html[Javadoc,role=link-external,window=_blank] * https://github.com/spring-projects/spring-data-commons/wiki[Wiki,role=link-external,window=_blank] ================================================ FILE: src/main/antora/modules/ROOT/pages/commons/upgrade.adoc ================================================ include::{commons}@data-commons::page$upgrade.adoc[] Once you’ve decided to upgrade your application, you can find detailed information regarding specific features in the rest of the document. Spring Data's documentation is specific to that version, so any information that you find in here will contain the most up-to-date changes that are in that version. ================================================ FILE: src/main/antora/modules/ROOT/pages/index.adoc ================================================ [[spring-data-key-value-reference-guide]] = Spring Data Key-Value :revnumber: {version} :revdate: {localdate} :feature-scroll: true Spring Data KeyValue provides connectivity and repository support for the in memory map structures. It eases development of applications with a consistent programming model that need to access key based storage and servers as foundation for custom adapters._ [horizontal] xref:keyvalue.adoc[Key/Value Storage] :: Support for built in key-value structures xref:repositories.adoc[Repositories] :: KeyValue Repositories https://github.com/spring-projects/spring-data-commons/wiki[Wiki] :: What's New, Upgrade Notes, Supported Versions, additional cross-version information. Oliver Gierke; Thomas Darimont; Christoph Strobl; Jay Bryant; Mark Paluch (C) 2008-{copyright-year} VMware, Inc. Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. ================================================ FILE: src/main/antora/modules/ROOT/pages/keyvalue/repository/map-repositories.adoc ================================================ [[key-value.repositories.map]] = Map Repositories Map repositories reside on top of the `KeyValueTemplate`. Using the default `PredicateQueryCreator` allows deriving query and sort expressions from the given method name, as the following example shows: [source, java] ---- @Configuration @EnableMapRepositories class KeyValueConfig { } interface PersonRepository implements CrudRepository { List findByLastname(String lastname); } ---- == Configuring the QueryEngine It is possible to change the `QueryEngine` and use a custom one instead of the default. The `EnableMapRepositories` annotation allows to configure the by supplying a `QueryEngineFactory` as well as the `QueryCreator` via according attributes. Please mind that the `QueryEngine` needs to be able to process queries created by the configured `QueryCreator`. == Storage Backend Configuration `KeySpaceStore` provides a simple storage facade that can be used along with the `keySpaceStoreRef` attribute of `EnableMapRepositories` to set the bean reference to the actual storage, overriding `mapType` settings. ==== [source,java] ---- @Configuration @EnableMapRepositories(keySpaceStoreRef = "store") <1> public class MapDbConfiguration { @Bean KeySpaceStore store(DB db) { <1> return new KeySpaceStore() { @Override public Map getKeySpace(String keyspace) { return db.hashMap(keyspace, ... } @Override public void clear() { // } }; } } ---- <1> reference the `KeySpaceStore` by bean name. ==== ================================================ FILE: src/main/antora/modules/ROOT/pages/keyvalue/template.adoc ================================================ [[key-value.template]] = Template API In its very basic shape, the `KeyValueTemplate` uses a `MapAdapter` that wraps a `ConcurrentHashMap` and that uses link:{spring-framework-docs}/core/expressions.html[Spring Expression Language] to run queries and sorting. NOTE: The used `KeyValueAdapter` does the heavy lifting when it comes to storing and retrieving data. The data structure influences performance and multi-threading behavior. You can use a different type or pre-initialize the adapter with some values, and you can do so by using various constructors on `MapKeyValueAdapter`, as the following example shows: ==== [source, java] ---- @Configuration class MyConfiguration { @Bean public KeyValueOperations mapKeyValueTemplate() { <1> return new KeyValueTemplate(keyValueAdapter()); } @Bean public KeyValueAdapter keyValueAdapter() { return new MapKeyValueAdapter(ConcurrentHashMap.class); <2> } } ---- <1> Defines a custom `KeyValueOperations` bean using the default bean name. See documentation and properties of `@EnableMapRepositories` for further customization. <2> Defines a custom `KeyValueAdapter` bean using a `ConcurrentHashMap` as storage that is used by `KeyValueTemplate`. ==== [[key-value.keyspaces]] == Keyspaces The following example shows a keyspace for a repository of `Person` objects: ==== [source, java] ---- @KeySpace("persons") class Person { @Id String id; String firstname; String lastname; } class User extends Person { String username; } template.findAllOf(Person.class); <1> template.findAllOf(User.class); <2> ---- <1> Returns all entities for the `persons` keyspace. <2> Returns only elements of type `User` stored in `persons` keyspace. ==== TIP: `@KeySpace` supports xref:keyvalue/value-expressions.adoc[Value Expressions] allowing dynamic keyspace configuration. [[key-value.keyspaces-custom]] === Custom KeySpace Annotation You can compose your own `KeySpace` annotations for a more domain-centric usage by annotating one of the attributes with `@AliasFor`. IMPORTANT: The composed annotation must inherit `@Persistent`. The following example shows a custom `@KeySpace` annotation: ==== [source, java] ---- @KeySpace @Persistent @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) static @interface CacheCentricAnnotation { @AliasFor(annotation = KeySpace.class, attribute = "value") String cacheRegion() default ""; } @CacheCentricAnnotation(cacheRegion = "customers") class Customer { //... } ---- ==== [[key-value.template-query]] == Querying Running queries is managed by a `QueryEngine`. As mentioned earlier, you can instruct the `KeyValueAdapter` to use an implementation-specific `QueryEngine` that allows access to native functionality. When used without further customization, queries can be run by using `SpELQueryEngine`. NOTE: For performance reasons, we highly recommend to make use of link:{spring-framework-docs}/core/expressions/evaluation.html#expressions-compiler-configuration[compiled SpEL Expressions]. ("`SpEL`" is short for "`Spring Expression Language`".) You can use the `-Dspring.expression.compiler.mode=IMMEDIATE` switch to enable it. The following example shows a query that uses the SpEL: ==== [source,java] ---- KeyValueQuery query = new KeyValueQuery("lastname == 'targaryen'"); List targaryens = template.find(query, Person.class); ---- ==== IMPORTANT: You must have getters and setters present to query properties when you use SpEL. [[key-value.template-sort]] == Sorting Depending on the store implementation provided by the adapter, entities might already be stored in some sorted way but do not necessarily have to be.Again, the underlying `QueryEngine` is capable of performing sort operations. When used without further customization, sorting is done by using a `SpelPropertyComparator` extracted from the `Sort` clause.The following example shows a query with a `Sort` clause: ==== [source, java] ---- KeyValueQuery query = new KeyValueQuery("lastname == 'baratheon'"); query.setSort(Sort.by(DESC, "age")); List targaryens = template.find(query, Person.class); ---- ==== IMPORTANT: Please note that you need to have getters and setters present to sort using SpEL. ================================================ FILE: src/main/antora/modules/ROOT/pages/keyvalue/value-expressions.adoc ================================================ include::{commons}@data-commons::page$value-expressions.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/keyvalue.adoc ================================================ [[key-value]] = KeyValue Spring Data KeyValue provides easy configuration and access to `Map` like structures that associate values with unique keys. It offers both low-level and high-level abstractions for interacting with the underlying data structure, freeing the user from infrastructural concerns. The key-value abstraction within Spring Data Key Value requires an `Adapter` that shields the native store implementation, freeing up `KeyValueTemplate` to work on top of any key-value pair-like structure. Keys are distributed across <>. Unless otherwise specified, the class name is used as the default keyspace for an entity. The following interface definition shows the `KeyValueOperations` interface, which is the heart of Spring Data Key-Value: ==== [source, java] ---- interface KeyValueOperations { T insert(T objectToInsert); <1> void update(Object objectToUpdate); <2> void delete(Class type); <3> T findById(Object id, Class type); <4> Iterable findAllOf(Class type); <5> Iterable find(KeyValueQuery query, Class type); <6> //... more functionality omitted. } ---- <1> Inserts the given entity and assigns an ID (if required). <2> Updates the given entity. <3> Removes all entities of the matching type. <4> Returns the entity of the given type with its matching ID. <5> Returns all entities of the matching type. <6> Returns a `List` of all entities of the given type that match the criteria of the query. ==== [[key-value.keyspaces]] == Keyspaces Keyspaces define the part of the data structure in which the entity should be kept. This concept is similar to collections in MongoDB and Elasticsearch, cores in Solr, and tables in JPA. By default, the keyspace of an entity is extracted from its type, but you can also store entities of different types within one keyspace. ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc ================================================ include::{commons}@data-commons::page$repositories/core-concepts.adoc[] [[redis.entity-persistence.state-detection-strategies]] include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc ================================================ include::{commons}@data-commons::page$repositories/core-domain-events.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc ================================================ [[core.extensions.querydsl]] = Querydsl Spring Data Redis does not support Querydsl. ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc ================================================ include::{commons}@data-commons::page$repositories/create-instances.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc ================================================ include::{commons}@data-commons::page$repositories/custom-implementations.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/definition.adoc ================================================ include::{commons}@data-commons::page$repositories/definition.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc ================================================ include::{commons}@data-commons::page$repositories/null-handling.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/object-mapping.adoc ================================================ include::{commons}@data-commons::page$object-mapping.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/projections.adoc ================================================ [[cassandra.projections]] = Projections include::{commons}@data-commons::page$repositories/projections.adoc[leveloffset=+1] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc ================================================ include::{commons}@data-commons::page$repositories/query-keywords-reference.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc ================================================ include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc ================================================ include::{commons}@data-commons::page$repositories/query-return-types-reference.adoc[] ================================================ FILE: src/main/antora/modules/ROOT/pages/repositories.adoc ================================================ [[keyvalyue.repositories]] = KeyValue Repositories This chapter explains the basic foundations of Spring Data repositories and KeyValue specifics. Before continuing to the specifics, make sure you have a sound understanding of the basic concepts. The goal of the Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores. ================================================ FILE: src/main/antora/resources/antora-resources/antora.yml ================================================ version: ${antora-component.version} prerelease: ${antora-component.prerelease} asciidoc: attributes: attribute-missing: 'warn' chomp: 'all' version: '${project.version}' copyright-year: '${current.year}' springversionshort: '${spring.short}' springversion: '${spring}' commons: '${springdata.commons.docs}' include-xml-namespaces: false spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}' spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}' spring-framework-docs: '{springdocsurl}' springjavadocurl: '${documentation.spring-javadoc-url}' spring-framework-javadoc: '{springjavadocurl}' springhateoasversion: '${spring-hateoas}' releasetrainversion: '${releasetrain}' store: KeyValue ================================================ FILE: src/main/java/org/springframework/data/keyvalue/annotation/KeySpace.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.annotation; import static java.lang.annotation.ElementType.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.data.annotation.Persistent; /** * Marker interface for methods with {@link Persistent} annotations indicating the presence of a dedicated keyspace the * entity should reside in. If present the value will be picked up for resolving the keyspace. The {@link #value()} * attribute supports Value Expressions to dynamically resolve the keyspace based on a per-operation basis. * *
 * @Persistent
 * @Retention(RetentionPolicy.RUNTIME)
 * @Target({ ElementType.TYPE })
 * static @interface CacheCentricAnnotation {
 *
 * 	@AliasFor(annotation = KeySpace.class, attribute = "value")
 * 	String cacheRegion() default "";
 * }
 *
 * @CacheCentricAnnotation(cacheRegion = "customers")
 * class Customer {
 * 	// ...
 * }
 * 
* * Can also be directly used on types to indicate the keyspace. * *
 * @KeySpace("persons")
 * public class Foo {
 *
 * }
 * 
* * @author Christoph Strobl * @author Mark Paluch */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value = { METHOD, TYPE }) public @interface KeySpace { /** * @return dedicated keyspace the entity should reside in. */ String value() default ""; } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/annotation/package-info.java ================================================ /** * Key-Value annotations for declarative keyspace configuration. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.annotation; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/aot/KeyValueRuntimeHints.java ================================================ /* * Copyright 2022-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.aot; import java.util.Arrays; import java.util.List; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; /** * {@link RuntimeHintsRegistrar} for KeyValue. * * @author Christoph Strobl * @since 3.0 */ class KeyValueRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { // REFLECTION hints.reflection().registerTypes( Arrays.asList( TypeReference.of(org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository.class), TypeReference.of(KeyValuePartTreeQuery.class)), hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); hints.reflection().registerType(TypeReference.of("java.util.Comparators.NaturalOrderComparator"), builder -> builder.withMethod("compare", List.of(TypeReference.of(Object.class), TypeReference.of(Object.class)), ExecutableMode.INVOKE)); hints.reflection().registerType(TypeReference.of("java.util.Comparators.NullComparator"), builder -> builder.withMethod("compare", List.of(TypeReference.of(Object.class), TypeReference.of(Object.class)), ExecutableMode.INVOKE)); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/aot/package-info.java ================================================ /** * Support classes for key-value ahead of time computation */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.aot; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/AbstractKeyValueAdapter.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Collection; import java.util.Comparator; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * Base implementation of {@link KeyValueAdapter} holds {@link QueryEngine} to delegate {@literal find} and * {@literal count} execution to. * * @author Christoph Strobl * @author Mark Paluch */ public abstract class AbstractKeyValueAdapter implements KeyValueAdapter { private final QueryEngine engine; /** * Creates new {@link AbstractKeyValueAdapter} with using the default query engine. */ protected AbstractKeyValueAdapter() { this((QueryEngine) null); } /** * Creates new {@link AbstractKeyValueAdapter} with using the default query engine and provided comparator for sorting. * * @param sortAccessor must not be {@literal null}. * @since 3.1.10 */ protected AbstractKeyValueAdapter(SortAccessor> sortAccessor) { this(new PredicateQueryEngine(sortAccessor)); } /** * Creates new {@link AbstractKeyValueAdapter} with using the default query engine. * * @param engine will be defaulted to {@link SpelQueryEngine} if {@literal null}. */ protected AbstractKeyValueAdapter(@Nullable QueryEngine engine) { this.engine = engine != null ? engine : new PredicateQueryEngine(); this.engine.registerAdapter(this); } /** * Get the {@link QueryEngine} used. * * @return */ protected QueryEngine getQueryEngine() { return engine; } @Override public @Nullable T get(Object id, String keyspace, Class type) { return type.cast(get(id, keyspace)); } @Override public @Nullable T delete(Object id, String keyspace, Class type) { return type.cast(delete(id, keyspace)); } @Override public Iterable find(KeyValueQuery query, String keyspace, Class type) { return engine.execute(query, keyspace, type); } @Override public Collection find(KeyValueQuery query, String keyspace) { return engine.execute(query, keyspace); } @Override public long count(KeyValueQuery query, String keyspace) { return engine.count(query, keyspace); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/CriteriaAccessor.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * Resolves the criteria object from given {@link KeyValueQuery}. * * @author Christoph Strobl * @author Mark Paluch * @param */ public interface CriteriaAccessor { /** * Checks and reads {@link KeyValueQuery#getCriteria()} of given {@link KeyValueQuery}. Might also apply additional * transformation to match the desired type. * * @param query must not be {@literal null}. * @return the criteria extracted from the query. Can be {@literal null}. * @throws IllegalArgumentException in case the criteria is not valid for usage with specific * {@link CriteriaAccessor}. */ @Nullable T resolve(KeyValueQuery query); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/DefaultIdentifierGenerator.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.core.TypeInformation; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** * Default implementation of {@link IdentifierGenerator} to generate identifiers of types {@link UUID}, String, * * @author Christoph Strobl * @author Oliver Gierke */ enum DefaultIdentifierGenerator implements IdentifierGenerator { INSTANCE; private final AtomicReference secureRandom = new AtomicReference<>(null); @Override @SuppressWarnings("unchecked") public T generateIdentifierOfType(TypeInformation identifierType) { Class type = identifierType.getType(); if (ClassUtils.isAssignable(UUID.class, type)) { return (T) UUID.randomUUID(); } else if (ClassUtils.isAssignable(String.class, type)) { return (T) UUID.randomUUID().toString(); } else if (ClassUtils.isAssignable(Integer.class, type)) { return (T) Integer.valueOf(getSecureRandom().nextInt()); } else if (ClassUtils.isAssignable(Long.class, type)) { return (T) Long.valueOf(getSecureRandom().nextLong()); } throw new InvalidDataAccessApiUsageException( String.format("Identifier cannot be generated for %s; Supported types are: UUID, String, Integer, and Long", identifierType.getType().getName())); } private SecureRandom getSecureRandom() { SecureRandom secureRandom = this.secureRandom.get(); if (secureRandom != null) { return secureRandom; } for (String algorithm : OsTools.secureRandomAlgorithmNames()) { try { secureRandom = SecureRandom.getInstance(algorithm); } catch (NoSuchAlgorithmException e) { // ignore and try next. } } if (secureRandom == null) { throw new InvalidDataAccessApiUsageException( String.format("Could not create SecureRandom instance for one of the algorithms '%s'", StringUtils.collectionToCommaDelimitedString(OsTools.secureRandomAlgorithmNames()))); } this.secureRandom.compareAndSet(null, secureRandom); return secureRandom; } /** * @author Christoph Strobl * @since 1.1.2 */ private static class OsTools { private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(); private static final List SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS = Arrays.asList("NativePRNGBlocking", "NativePRNGNonBlocking", "NativePRNG", "SHA1PRNG"); private static final List SECURE_RANDOM_ALGORITHMS_WINDOWS = Arrays.asList("SHA1PRNG", "Windows-PRNG"); static List secureRandomAlgorithmNames() { return OPERATING_SYSTEM_NAME.contains("win") ? SECURE_RANDOM_ALGORITHMS_WINDOWS : SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS; } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/ForwardingCloseableIterator.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Iterator; import org.jspecify.annotations.Nullable; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; /** * Forwards {@link CloseableIterator} invocations to the configured {@link Iterator} delegate. * * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke * @author Mark Paluch */ public class ForwardingCloseableIterator implements CloseableIterator { private final Iterator delegate; private final @Nullable Runnable closeHandler; /** * Creates a new {@link ForwardingCloseableIterator}. * * @param delegate must not be {@literal null}. */ public ForwardingCloseableIterator(Iterator delegate) { this(delegate, null); } /** * Creates a new {@link ForwardingCloseableIterator} that invokes the configured {@code closeHandler} on * {@link #close()}. * * @param delegate must not be {@literal null}. * @param closeHandler may be {@literal null}. */ public ForwardingCloseableIterator(Iterator delegate, @Nullable Runnable closeHandler) { Assert.notNull(delegate, "Delegate iterator must not be null"); this.delegate = delegate; this.closeHandler = closeHandler; } @Override public boolean hasNext() { return delegate.hasNext(); } @Override public T next() { return delegate.next(); } @Override public void close() { if (closeHandler != null) { closeHandler.run(); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/GeneratingIdAccessor.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.util.Assert; /** * {@link IdentifierAccessor} adding a {@link #getOrGenerateIdentifier()} to automatically generate an identifier and * set it on the underling bean instance. * * @author Oliver Gierke * @author Mark Paluch * @see #getOrGenerateIdentifier() */ class GeneratingIdAccessor implements IdentifierAccessor { private final PersistentPropertyAccessor accessor; private final PersistentProperty identifierProperty; private final IdentifierGenerator generator; /** * Creates a new {@link GeneratingIdAccessor} using the given {@link PersistentPropertyAccessor}, identifier property * and {@link IdentifierGenerator}. * * @param accessor must not be {@literal null}. * @param identifierProperty must not be {@literal null}. * @param generator must not be {@literal null}. */ GeneratingIdAccessor(PersistentPropertyAccessor accessor, PersistentProperty identifierProperty, IdentifierGenerator generator) { Assert.notNull(accessor, "PersistentPropertyAccessor must not be null"); Assert.notNull(identifierProperty, "Identifier property must not be null"); Assert.notNull(generator, "IdentifierGenerator must not be null"); this.accessor = accessor; this.identifierProperty = identifierProperty; this.generator = generator; } @Override public @Nullable Object getIdentifier() { return accessor.getProperty(identifierProperty); } /** * Returns the identifier value of the backing bean or generates a new one using the configured * {@link IdentifierGenerator}. * * @return */ Object getOrGenerateIdentifier() { Object existingIdentifier = getIdentifier(); if (existingIdentifier != null) { return existingIdentifier; } Object generatedIdentifier = generator.generateIdentifierOfType(identifierProperty.getTypeInformation()); accessor.setProperty(identifierProperty, generatedIdentifier); return generatedIdentifier; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/IdentifierGenerator.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.springframework.data.core.TypeInformation; /** * API for components generating identifiers. * * @author Christoph Strobl * @author Oliver Gierke */ public interface IdentifierGenerator { /** * Creates an identifier of the given type. * * @param type must not be {@literal null}. * @return an identifier of the given type. */ T generateIdentifierOfType(TypeInformation type); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/IterableConverter.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; /** * Converter capable of transforming a given {@link Iterable} into a collection type. * * @author Christoph Strobl * @author Mark Paluch */ public final class IterableConverter { private IterableConverter() {} /** * Converts a given {@link Iterable} into a {@link List} * * @param source * @return {@link Collections#emptyList()} when source is {@literal null}. */ @Contract("_ -> !null") public static List toList(@Nullable Iterable source) { if(source == null) { return Collections.emptyList(); } if (source instanceof List) { return (List) source; } if (source instanceof Collection) { return new ArrayList<>((Collection) source); } List result = new ArrayList<>(); for (T value : source) { result.add(value); } return result; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Collection; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.util.CloseableIterator; /** * {@link KeyValueAdapter} unifies access and shields the underlying key/value specific implementation. * * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch */ public interface KeyValueAdapter extends DisposableBean { /** * Add object with given id to keyspace. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return the item previously associated with the id. */ @Nullable Object put(Object id, Object item, String keyspace); /** * Check if a object with given id exists in keyspace. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return true if item of type with id exists. */ boolean contains(Object id, String keyspace); /** * Get the object with given id from keyspace. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return {@literal null} in case no matching item exists. */ @Nullable Object get(Object id, String keyspace); /** * Get the object with given id from keyspace. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @param type must not be {@literal null}. * @return {@literal null} in case no matching item exists. * @since 1.1 */ @Nullable T get(Object id, String keyspace, Class type); /** * Delete and return the object with given type and id. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return {@literal null} if object could not be found */ @Nullable Object delete(Object id, String keyspace); /** * Delete and return the object with given type and id. * * @param id must not be {@literal null}. * @param keyspace must not be {@literal null}. * @param type must not be {@literal null}. * @return {@literal null} if object could not be found * @since 1.1 */ @Nullable T delete(Object id, String keyspace, Class type); /** * Get all elements for given keyspace. * * @param keyspace must not be {@literal null}. * @return empty {@link Collection} if nothing found. */ Iterable getAllOf(String keyspace); /** * Get all elements for given keyspace. * * @param keyspace must not be {@literal null}. * @param type must not be {@literal null}. * @return empty {@link Collection} if nothing found. * @since 2.5 */ @SuppressWarnings("unchecked") default Iterable getAllOf(String keyspace, Class type) { return (Iterable) getAllOf(keyspace); } /** * Returns a {@link CloseableIterator} that iterates over all entries. * * @param keyspace must not be {@literal null}. * @return */ CloseableIterator> entries(String keyspace); /** * Returns a {@link CloseableIterator} that iterates over all entries. * * @param keyspace must not be {@literal null}. * @param type must not be {@literal null}. * @return * @since 2.5 */ @SuppressWarnings("unchecked") default CloseableIterator> entries(String keyspace, Class type) { return (CloseableIterator) entries(keyspace); } /** * Remove all objects of given type. * * @param keyspace must not be {@literal null}. */ void deleteAllOf(String keyspace); /** * Removes all objects. */ void clear(); /** * Find all matching objects within {@literal keyspace}. * * @param query must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return empty {@link Collection} if no match found. */ default Iterable find(KeyValueQuery query, String keyspace) { return find(query, keyspace, Object.class); } /** * @param query must not be {@literal null}. * @param keyspace must not be {@literal null}. * @param type must not be {@literal null}. * @return empty {@link Collection} if no match found. * @since 1.1 */ Iterable find(KeyValueQuery query, String keyspace, Class type); /** * Count number of objects within {@literal keyspace}. * * @param keyspace must not be {@literal null}. * @return */ long count(String keyspace); /** * Count all matching objects within {@literal keyspace}. * * @param query must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return */ long count(KeyValueQuery query, String keyspace); /** * Determine whether result of given {@link KeyValueQuery} within {@literal keyspace} contains at least one element. * * @param query must not be {@literal null}. * @param keyspace must not be {@literal null}. * @return * @since 2.7 */ default boolean exists(KeyValueQuery query, String keyspace) { return count(query, keyspace) > 0; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/KeyValueCallback.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; /** * Generic callback interface for code that operates on a {@link KeyValueAdapter}. This is particularly useful for * delegating code that needs to work closely on the underlying key/value store implementation. * * @author Christoph Strobl * @author Mark Paluch * @param */ public interface KeyValueCallback { /** * Gets called by {@code KeyValueTemplate#execute(KeyValueCallback)}. Allows for returning a result object created * within the callback, i.e. a domain object or a collection of domain objects. * * @param adapter * @return */ @Nullable T doInKeyValue(KeyValueAdapter adapter); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/KeyValueOperations.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.annotation.KeySpace; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.mapping.context.MappingContext; /** * Interface that specifies a basic set of key/value operations. Implemented by {@link KeyValueTemplate}. * * @author Christoph Strobl * @author Mark Paluch */ public interface KeyValueOperations extends DisposableBean { /** * Add given object. Object needs to have id property to which a generated value will be assigned. * * @param objectToInsert * @return the inserted object. */ T insert(T objectToInsert); /** * Add object with given id. * * @param id must not be {@literal null}. * @param objectToInsert must not be {@literal null}. * @return the inserted object. */ T insert(Object id, T objectToInsert); /** * Get all elements of given type. Respects {@link KeySpace} if present and therefore returns all elements that can be * assigned to requested type. * * @param type must not be {@literal null}. * @return empty iterable if no elements found. */ Iterable findAll(Class type); /** * Get all elements ordered by sort. Respects {@link KeySpace} if present and therefore returns all elements that can * be assigned to requested type. * * @param sort must not be {@literal null}. * @param type must not be {@literal null}. * @return */ Iterable findAll(Sort sort, Class type); /** * Get element of given type with given id. Respects {@link KeySpace} if present and therefore returns all elements * that can be assigned to requested type. * * @param id must not be {@literal null}. * @param type must not be {@literal null}. * @return {@link Optional#empty()} if not found. */ Optional findById(Object id, Class type); /** * Execute operation against underlying store. * * @param action must not be {@literal null}. * @return */ @Nullable T execute(KeyValueCallback action); /** * Get all elements matching the given query.
* Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type.. * * @param query must not be {@literal null}. * @param type must not be {@literal null}. * @return empty iterable if no match found. */ Iterable find(KeyValueQuery query, Class type); /** * Get all elements in given range. Respects {@link KeySpace} if present and therefore returns all elements that can * be assigned to requested type. * * @param offset * @param rows * @param type must not be {@literal null}. * @return */ Iterable findInRange(long offset, int rows, Class type); /** * Get all elements in given range ordered by sort. Respects {@link KeySpace} if present and therefore returns all * elements that can be assigned to requested type. * * @param offset * @param rows * @param sort * @param type * @return */ Iterable findInRange(long offset, int rows, Sort sort, Class type); /** * @param objectToUpdate must not be {@literal null}. * @return the updated object. */ T update(T objectToUpdate); /** * @param id must not be {@literal null}. * @param objectToUpdate must not be {@literal null}. * @return the updated object. */ T update(Object id, T objectToUpdate); /** * Remove all elements of type. Respects {@link KeySpace} if present and therefore removes all elements that can be * assigned to requested type. * * @param type must not be {@literal null}. */ void delete(Class type); /** * @param objectToDelete must not be {@literal null}. * @return */ @Nullable T delete(T objectToDelete); /** * Delete item of type with given id. * * @param id must not be {@literal null}. * @param type must not be {@literal null}. * @return the deleted item or {@literal null} if no match found. */ @Nullable T delete(Object id, Class type); /** * Total number of elements with given type available. Respects {@link KeySpace} if present and therefore counts all * elements that can be assigned to requested type. * * @param type must not be {@literal null}. * @return */ long count(Class type); /** * Total number of elements matching given query. Respects {@link KeySpace} if present and therefore counts all * elements that can be assigned to requested type. * * @param query * @param type * @return */ long count(KeyValueQuery query, Class type); /** * Determine whether result of given {@link KeyValueQuery} contains at least one element. * * @param query * @param type * @return * @since 2.7 */ boolean exists(KeyValueQuery query, Class type); /** * @return mapping context in use. */ MappingContext getMappingContext(); /** * @return {@link KeyValueAdapter} in use. * @since 3.2.4 */ KeyValueAdapter getKeyValueAdapter(); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/KeyValuePersistenceExceptionTranslator.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.NoSuchElementException; import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.util.Assert; /** * Simple {@link PersistenceExceptionTranslator} implementation for key/value stores that converts the given runtime * exception to an appropriate exception from the {@code org.springframework.dao} hierarchy. * * @author Christoph Strobl * @author Mark Paluch */ public class KeyValuePersistenceExceptionTranslator implements PersistenceExceptionTranslator { @Override @SuppressWarnings("NullAway") public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException exception) { Assert.notNull(exception, "Exception must not be null"); if (exception instanceof DataAccessException) { return (DataAccessException) exception; } if (exception instanceof NoSuchElementException || exception instanceof IndexOutOfBoundsException || exception instanceof IllegalStateException) { return new DataRetrievalFailureException(exception.getMessage(), exception); } if (exception.getClass().getName().startsWith("java")) { return new UncategorizedKeyValueException(exception.getMessage(), exception); } return null; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.dao.DataAccessException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.event.KeyValueEvent; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** * Basic implementation of {@link KeyValueOperations}. * * @author Christoph Strobl * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch * @author Mert Zeybekler * @author Adeyemi Abass */ public class KeyValueTemplate implements KeyValueOperations, ApplicationEventPublisherAware { private static final PersistenceExceptionTranslator DEFAULT_PERSISTENCE_EXCEPTION_TRANSLATOR = new KeyValuePersistenceExceptionTranslator(); private final KeyValueAdapter adapter; private final MappingContext, ? extends KeyValuePersistentProperty> mappingContext; private final IdentifierGenerator identifierGenerator; private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_PERSISTENCE_EXCEPTION_TRANSLATOR; private @Nullable ApplicationEventPublisher eventPublisher; private boolean publishEvents = true; private @SuppressWarnings("rawtypes") Set> eventTypesToPublish = Collections .emptySet(); /** * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} with a default * {@link KeyValueMappingContext}. * * @param adapter must not be {@literal null}. */ public KeyValueTemplate(KeyValueAdapter adapter) { this(adapter, new KeyValueMappingContext<>()); } /** * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link MappingContext}. * * @param adapter must not be {@literal null}. * @param mappingContext must not be {@literal null}. */ public KeyValueTemplate(KeyValueAdapter adapter, MappingContext, ? extends KeyValuePersistentProperty> mappingContext) { this(adapter, mappingContext, DefaultIdentifierGenerator.INSTANCE); } /** * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link MappingContext}. * * @param adapter must not be {@literal null}. * @param mappingContext must not be {@literal null}. * @param identifierGenerator must not be {@literal null}. * @since 2.4 */ public KeyValueTemplate(KeyValueAdapter adapter, MappingContext, ? extends KeyValuePersistentProperty> mappingContext, IdentifierGenerator identifierGenerator) { Assert.notNull(adapter, "Adapter must not be null"); Assert.notNull(mappingContext, "MappingContext must not be null"); Assert.notNull(identifierGenerator, "IdentifierGenerator must not be null"); this.adapter = adapter; this.mappingContext = mappingContext; this.identifierGenerator = identifierGenerator; } /** * Set the {@link PersistenceExceptionTranslator} used for converting {@link RuntimeException}. * * @param exceptionTranslator must not be {@literal null}. */ public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) { Assert.notNull(exceptionTranslator, "ExceptionTranslator must not be null"); this.exceptionTranslator = exceptionTranslator; } /** * Define the event types to publish via {@link ApplicationEventPublisher}. * * @param eventTypesToPublish use {@literal null} or {@link Collections#emptySet()} to stop publishing. */ @SuppressWarnings("rawtypes") public void setEventTypesToPublish(Set> eventTypesToPublish) { if (CollectionUtils.isEmpty(eventTypesToPublish)) { this.publishEvents = false; } else { this.publishEvents = true; this.eventTypesToPublish = Collections.unmodifiableSet(eventTypesToPublish); } } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } @Override public T insert(T objectToInsert) { KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToInsert); GeneratingIdAccessor generatingIdAccessor = new GeneratingIdAccessor(entity.getPropertyAccessor(objectToInsert), entity.getRequiredIdProperty(), identifierGenerator); Object id = generatingIdAccessor.getOrGenerateIdentifier(); return insert(id, objectToInsert); } @Override public T insert(Object id, T objectToInsert) { Assert.notNull(id, "Id for object to be inserted must not be null"); Assert.notNull(objectToInsert, "Object to be inserted must not be null"); String keyspace = resolveKeySpace(objectToInsert.getClass()); potentiallyPublishEvent(KeyValueEvent.beforeInsert(id, keyspace, objectToInsert.getClass(), objectToInsert)); execute((KeyValueCallback) adapter -> { if (adapter.contains(id, keyspace)) { throw new DuplicateKeyException( String.format("Cannot insert existing object with id %s; Please use update", id)); } adapter.put(id, objectToInsert, keyspace); return null; }); potentiallyPublishEvent(KeyValueEvent.afterInsert(id, keyspace, objectToInsert.getClass(), objectToInsert)); return objectToInsert; } @Override public T update(T objectToUpdate) { KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToUpdate); if (!entity.hasIdProperty()) { throw new InvalidDataAccessApiUsageException( String.format("Cannot determine id for type %s", ClassUtils.getUserClass(objectToUpdate))); } return update(entity.getIdentifierAccessor(objectToUpdate).getRequiredIdentifier(), objectToUpdate); } @Override public T update(Object id, T objectToUpdate) { Assert.notNull(id, "Id for object to be inserted must not be null"); Assert.notNull(objectToUpdate, "Object to be updated must not be null"); String keyspace = resolveKeySpace(objectToUpdate.getClass()); potentiallyPublishEvent(KeyValueEvent.beforeUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate)); Object existing = execute(adapter -> adapter.put(id, objectToUpdate, keyspace)); potentiallyPublishEvent( KeyValueEvent.afterUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate, existing)); return objectToUpdate; } @Override public Iterable findAll(Class type) { Assert.notNull(type, "Type to fetch must not be null"); return executeRequired(adapter -> { String keyspace = resolveKeySpace(type); Iterable values = adapter.getAllOf(keyspace, type); ArrayList filtered = new ArrayList<>(); for (Object candidate : values) { if (typeCheck(type, candidate)) { filtered.add(type.cast(candidate)); } } return filtered; }); } @Override public Optional findById(Object id, Class type) { Assert.notNull(id, "Id for object to be found must not be null"); Assert.notNull(type, "Type to fetch must not be null"); String keyspace = resolveKeySpace(type); potentiallyPublishEvent(KeyValueEvent.beforeGet(id, keyspace, type)); T result = execute(adapter -> { Object value = adapter.get(id, keyspace, type); if (value == null || typeCheck(type, value)) { return type.cast(value); } return null; }); potentiallyPublishEvent(KeyValueEvent.afterGet(id, keyspace, type, result)); return Optional.ofNullable(result); } @Override public void delete(Class type) { Assert.notNull(type, "Type to delete must not be null"); String keyspace = resolveKeySpace(type); potentiallyPublishEvent(KeyValueEvent.beforeDropKeySpace(keyspace, type)); execute((KeyValueCallback) adapter -> { adapter.deleteAllOf(keyspace); return null; }); potentiallyPublishEvent(KeyValueEvent.afterDropKeySpace(keyspace, type)); } @SuppressWarnings("unchecked") @Override public @Nullable T delete(T objectToDelete) { Class type = (Class) ClassUtils.getUserClass(objectToDelete); KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToDelete); return delete(entity.getIdentifierAccessor(objectToDelete).getRequiredIdentifier(), type); } @Override public @Nullable T delete(Object id, Class type) { Assert.notNull(id, "Id for object to be deleted must not be null"); Assert.notNull(type, "Type to delete must not be null"); String keyspace = resolveKeySpace(type); potentiallyPublishEvent(KeyValueEvent.beforeDelete(id, keyspace, type)); T result = execute(adapter -> adapter.delete(id, keyspace, type)); potentiallyPublishEvent(KeyValueEvent.afterDelete(id, keyspace, type, result)); return result; } @Override public long count(Class type) { Assert.notNull(type, "Type for count must not be null"); String keyspace = resolveKeySpace(type); return adapter.count(keyspace); } @Override public @Nullable T execute(KeyValueCallback action) { Assert.notNull(action, "KeyValueCallback must not be null"); try { return action.doInKeyValue(this.adapter); } catch (RuntimeException e) { throw resolveExceptionIfPossible(e); } } /** * Execute {@link KeyValueCallback} and require a non-{@literal null} return value. * * @param action * @param * @return */ protected T executeRequired(KeyValueCallback action) { T result = execute(action); if (result != null) { return result; } throw new IllegalStateException(String.format("KeyValueCallback %s returned null value", action)); } @Override public Iterable find(KeyValueQuery query, Class type) { return executeRequired((KeyValueCallback>) adapter -> { Iterable result = adapter.find(query, resolveKeySpace(type), type); List filtered = new ArrayList<>(); for (Object candidate : result) { if (typeCheck(type, candidate)) { filtered.add(type.cast(candidate)); } } return filtered; }); } @SuppressWarnings("rawtypes") @Override public Iterable findAll(Sort sort, Class type) { return find(new KeyValueQuery(sort), type); } @SuppressWarnings("rawtypes") @Override public Iterable findInRange(long offset, int rows, Class type) { return find(new KeyValueQuery().skip(offset).limit(rows), type); } @SuppressWarnings("rawtypes") @Override public Iterable findInRange(long offset, int rows, Sort sort, Class type) { return find(new KeyValueQuery(sort).skip(offset).limit(rows), type); } @Override public long count(KeyValueQuery query, Class type) { return executeRequired(adapter -> adapter.count(query, resolveKeySpace(type))); } @Override public boolean exists(KeyValueQuery query, Class type) { return executeRequired(adapter -> adapter.exists(query, resolveKeySpace(type))); } @Override public MappingContext getMappingContext() { return this.mappingContext; } @Override public KeyValueAdapter getKeyValueAdapter() { return adapter; } @Override public void destroy() throws Exception { this.adapter.clear(); } private KeyValuePersistentEntity getKeyValuePersistentEntity(Object objectToInsert) { return this.mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(objectToInsert)); } private String resolveKeySpace(Class type) { String keyspace = this.mappingContext.getRequiredPersistentEntity(type).getKeySpace(); Assert.notNull(keyspace, "Keyspace must not be null"); return keyspace; } private RuntimeException resolveExceptionIfPossible(RuntimeException e) { DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(e); return translatedException != null ? translatedException : e; } @SuppressWarnings("rawtypes") private void potentiallyPublishEvent(KeyValueEvent event) { if (eventPublisher == null) { return; } if (publishEvents && (eventTypesToPublish.isEmpty() || eventTypesToPublish.contains(event.getClass()))) { eventPublisher.publishEvent(event); } } private static boolean typeCheck(Class requiredType, @Nullable Object candidate) { return candidate == null || ClassUtils.isAssignable(requiredType, candidate.getClass()); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/PathSortAccessor.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Comparator; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * @author Christoph Strobl * @since 3.1.10 */ public class PathSortAccessor implements SortAccessor> { @Override public @Nullable Comparator resolve(KeyValueQuery query) { if (query.getSort().isUnsorted()) { return null; } Optional> comparator = Optional.empty(); for (Order order : query.getSort()) { PropertyPathComparator pathSort = new PropertyPathComparator<>(order.getProperty()); if (Direction.DESC.equals(order.getDirection())) { pathSort.desc(); if (!NullHandling.NATIVE.equals(order.getNullHandling())) { pathSort = NullHandling.NULLS_FIRST.equals(order.getNullHandling()) ? pathSort.nullsFirst() : pathSort.nullsLast(); } } if (!comparator.isPresent()) { comparator = Optional.of(pathSort); } else { PropertyPathComparator pathSortToUse = pathSort; comparator = comparator.map(it -> it.thenComparing(pathSortToUse)); } } return comparator.orElseThrow( () -> new IllegalStateException("No sort definitions have been added to this CompoundComparator to compare")); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.lang.Contract; /** * {@link QueryEngine} implementation specific for executing {@link Predicate} based {@link KeyValueQuery} against * {@link KeyValueAdapter}. * * @author Christoph Strobl * @since 3.3 */ public class PredicateQueryEngine extends QueryEngine, Comparator> { public static final PredicateQueryEngine INSTANCE = new PredicateQueryEngine(); /** * Creates a new {@link PredicateQueryEngine}. */ public PredicateQueryEngine() { this(new PathSortAccessor()); } /** * Creates a new query engine using provided {@link SortAccessor accessor} for sorting results. */ public PredicateQueryEngine(SortAccessor> sortAccessor) { super(new CriteriaAccessor<>() { @Override public @Nullable Predicate resolve(KeyValueQuery query) { return (Predicate) query.getCriteria(); } }, sortAccessor); } @Override public Collection execute(@Nullable Predicate criteria, @Nullable Comparator sort, long offset, int rows, String keyspace) { return sortAndFilterMatchingRange(getRequiredAdapter().getAllOf(keyspace), criteria, sort, offset, rows); } @Override public long count(@Nullable Predicate criteria, String keyspace) { return filterMatchingRange(IterableConverter.toList(getRequiredAdapter().getAllOf(keyspace)), criteria, -1, -1) .size(); } @SuppressWarnings({ "unchecked", "rawtypes" }) private List sortAndFilterMatchingRange(Iterable source, @Nullable Predicate criteria, @Nullable Comparator sort, long offset, int rows) { List tmp = IterableConverter.toList(source); if (sort != null) { tmp.sort(sort); } return filterMatchingRange(tmp, criteria, offset, rows); } @Contract("!null, _, _, _ -> !null") private static List filterMatchingRange(List source, @Nullable Predicate criteria, long offset, int rows) { Stream stream = source.stream(); if (criteria != null) { stream = stream.filter((Predicate) criteria); } if (offset > 0) { stream = stream.skip(offset); } if (rows > 0) { stream = stream.limit(rows); } return stream.collect(Collectors.toList()); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/PropertyPathComparator.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.data.core.PropertyPath; import org.springframework.lang.Contract; /** * {@link Comparator} implementation to compare objects based on a {@link PropertyPath}. This comparator obtains the * value at {@link PropertyPath} from the {@link #compare(Object, Object) given comparison objects} and then performs * the comparison. * * @author Christoph Strobl * @author Mark Paluch * @since 3.1.10 */ public class PropertyPathComparator implements Comparator { private static final Comparator NULLS_FIRST = Comparator.nullsFirst(Comparator.naturalOrder()); private static final Comparator NULLS_LAST = Comparator.nullsLast(Comparator.naturalOrder()); private final String path; private boolean asc = true; private boolean nullsFirst = true; private final Map, PropertyPath> pathCache = new HashMap<>(2); public PropertyPathComparator(String path) { this.path = path; } @Override public int compare(@Nullable T o1, @Nullable T o2) { if (o1 == null && o2 == null) { return 0; } if (o1 == null) { return nullsFirst ? 1 : -1; } if (o2 == null) { return nullsFirst ? 1 : -1; } PropertyPath propertyPath = pathCache.computeIfAbsent(o1.getClass(), it -> PropertyPath.from(path, it)); Object value1 = getCompareValue(o1, propertyPath); Object value2 = getCompareValue(o2, propertyPath); return getComparator().compare(value1, value2) * (asc ? 1 : -1); } protected @Nullable Object getCompareValue(S object, PropertyPath propertyPath) { return new SimplePropertyPathAccessor<>(object).getValue(propertyPath); } @SuppressWarnings("unchecked") private Comparator<@Nullable Object> getComparator() { return (Comparator) (nullsFirst ? NULLS_FIRST : NULLS_LAST); } /** * Sort {@literal ascending}. * * @return */ @Contract("-> this") public PropertyPathComparator<@Nullable T> asc() { this.asc = true; return this; } /** * Sort {@literal descending}. * * @return */ @Contract("-> this") public PropertyPathComparator<@Nullable T> desc() { this.asc = false; return this; } /** * Sort {@literal null} values first. * * @return */ @Contract("-> this") public PropertyPathComparator<@Nullable T> nullsFirst() { this.nullsFirst = true; return this; } /** * Sort {@literal null} values last. * * @return */ @Contract("-> this") public PropertyPathComparator<@Nullable T> nullsLast() { this.nullsFirst = false; return this; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/QueryEngine.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Collection; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * Base implementation for accessing and executing {@link KeyValueQuery} against a {@link KeyValueAdapter}. * * @author Christoph Strobl * @author Mark Paluch * @param * @param * @param */ public abstract class QueryEngine { private final Optional> criteriaAccessor; private final Optional> sortAccessor; private @Nullable ADAPTER adapter; public QueryEngine(@Nullable CriteriaAccessor criteriaAccessor, @Nullable SortAccessor sortAccessor) { this.criteriaAccessor = Optional.ofNullable(criteriaAccessor); this.sortAccessor = Optional.ofNullable(sortAccessor); } /** * Extract query attributes and delegate to concrete execution. * * @param query * @param keyspace * @return */ public Collection execute(KeyValueQuery query, String keyspace) { CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null); SORT sort = this.sortAccessor.map(it -> it.resolve(query)).orElse(null); return execute(criteria, sort, query.getOffset(), query.getRows(), keyspace); } /** * Extract query attributes and delegate to concrete execution. * * @param query * @param keyspace * @return */ public Collection execute(KeyValueQuery query, String keyspace, Class type) { CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null); SORT sort = this.sortAccessor.map(it -> it.resolve(query)).orElse(null); return execute(criteria, sort, query.getOffset(), query.getRows(), keyspace, type); } /** * Extract query attributes and delegate to concrete execution. * * @param query * @param keyspace * @return */ public long count(KeyValueQuery query, String keyspace) { CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null); return count(criteria, keyspace); } /** * @param criteria * @param sort * @param offset * @param rows * @param keyspace * @return */ public abstract Collection execute(@Nullable CRITERIA criteria, @Nullable SORT sort, long offset, int rows, String keyspace); /** * @param criteria * @param sort * @param offset * @param rows * @param keyspace * @param type * @return * @since 1.1 */ @SuppressWarnings("unchecked") public Collection execute(@Nullable CRITERIA criteria, @Nullable SORT sort, long offset, int rows, String keyspace, Class type) { return (Collection) execute(criteria, sort, offset, rows, keyspace); } /** * @param criteria * @param keyspace * @return */ public abstract long count(@Nullable CRITERIA criteria, String keyspace); /** * Get the {@link KeyValueAdapter} used. * * @return */ protected @Nullable ADAPTER getAdapter() { return this.adapter; } /** * Get the required {@link KeyValueAdapter} used or throw {@link IllegalStateException} if the adapter is not set. * * @return the required {@link KeyValueAdapter}. * @throws IllegalStateException if the adapter is not set. */ protected ADAPTER getRequiredAdapter() { ADAPTER adapter = getAdapter(); if (adapter != null) { return adapter; } throw new IllegalStateException("Required KeyValueAdapter is not set"); } /** * @param adapter */ @SuppressWarnings("unchecked") public void registerAdapter(KeyValueAdapter adapter) { if (this.adapter == null) { this.adapter = (ADAPTER) adapter; } else { throw new IllegalArgumentException("Cannot register more than one adapter for this QueryEngine"); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/QueryEngineFactory.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; /** * Interface for {@code QueryEngineFactory} implementations that provide a {@link QueryEngine} object as part of the * configuration. *

* The factory is used during configuration to supply the query engine to be used. When configured, a * {@code QueryEngineFactory} can be instantiated by accepting a {@link SortAccessor} in its constructor. Otherwise, * implementations are expected to declare a no-args constructor. * * @author Mark Paluch * @since 3.3.1 */ public interface QueryEngineFactory { /** * Factory method for creating a {@link QueryEngine}. * * @return the query engine. */ QueryEngine create(); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SimplePropertyPathAccessor.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanWrapper; import org.springframework.data.core.PropertyPath; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; /** * @author Christoph Strobl * @since 3.1.10 */ public class SimplePropertyPathAccessor { private final Object root; public SimplePropertyPathAccessor(Object source) { this.root = source; } public @Nullable Object getValue(PropertyPath path) { Object currentValue = root; for (PropertyPath current : path) { currentValue = wrap(currentValue).getPropertyValue(current.getSegment()); if (currentValue == null) { break; } } return currentValue; } BeanWrapper wrap(Object o) { return new DirectFieldAccessFallbackBeanWrapper(o); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SortAccessor.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * Resolves the {@link Sort} object from given {@link KeyValueQuery} and potentially converts it into a store specific * representation that can be used by the {@link QueryEngine} implementation. * * @author Christoph Strobl * @author Mark Paluch * @param */ public interface SortAccessor { /** * Reads {@link KeyValueQuery#getSort()} of given {@link KeyValueQuery} and applies required transformation to match * the desired type. * * @param query must not be {@literal null}. * @return {@literal null} in case {@link Sort} has not been defined on {@link KeyValueQuery}. */ @Nullable T resolve(KeyValueQuery query); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SpelCriteria.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.Assert; /** * {@link SpelCriteria} allows to pass on a {@link SpelExpression} and {@link EvaluationContext} to the actual query * processor. This decouples the {@link SpelExpression} from the context it is used in. * * @author Christoph Strobl * @author Oliver Gierke */ public class SpelCriteria { private final SpelExpression expression; private final EvaluationContext context; /** * Creates a new {@link SpelCriteria} for the given {@link SpelExpression}. * * @param expression must not be {@literal null}. */ public SpelCriteria(SpelExpression expression) { this(expression, SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build()); } /** * Creates new {@link SpelCriteria}. * * @param expression must not be {@literal null}. * @param context must not be {@literal null}. */ public SpelCriteria(SpelExpression expression, EvaluationContext context) { Assert.notNull(expression, "SpEL expression must not be null"); Assert.notNull(context, "EvaluationContext must not be null"); this.expression = expression; this.context = context; } /** * @return will never be {@literal null}. */ public EvaluationContext getContext() { return context; } /** * @return will never be {@literal null}. */ public SpelExpression getExpression() { return expression; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SpelCriteriaAccessor.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** * {@link CriteriaAccessor} implementation capable of {@link SpelExpression}s. * * @author Christoph Strobl * @author Oliver Gierke */ class SpelCriteriaAccessor implements CriteriaAccessor { private final SpelExpressionParser parser; /** * Creates a new {@link SpelCriteriaAccessor} using the given {@link SpelExpressionParser}. * * @param parser must not be {@literal null}. */ public SpelCriteriaAccessor(SpelExpressionParser parser) { Assert.notNull(parser, "SpelExpressionParser must not be null"); this.parser = parser; } @Override public @Nullable SpelCriteria resolve(KeyValueQuery query) { if (query.getCriteria() == null) { return null; } if (query.getCriteria() instanceof SpelExpression) { return new SpelCriteria((SpelExpression) query.getCriteria()); } if (query.getCriteria() instanceof String) { return new SpelCriteria(parser.parseRaw((String) query.getCriteria())); } if (query.getCriteria() instanceof SpelCriteria) { return (SpelCriteria) query.getCriteria(); } throw new IllegalArgumentException("Cannot create SpelCriteria for " + query.getCriteria()); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SpelPropertyComparator.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Comparator; import org.jspecify.annotations.Nullable; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.lang.Contract; import org.springframework.util.Assert; /** * {@link Comparator} implementation using {@link SpelExpression}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch * @param */ public class SpelPropertyComparator implements Comparator { private static final Comparator NULLS_FIRST = Comparator.nullsFirst(Comparator.naturalOrder()); private static final Comparator NULLS_LAST = Comparator.nullsLast(Comparator.naturalOrder()); private final String path; private final SpelExpressionParser parser; private boolean asc = true; private boolean nullsFirst = true; private @Nullable SpelExpression expression; /** * Create new {@link SpelPropertyComparator} for the given property path an {@link SpelExpressionParser}. * * @param path must not be {@literal null} or empty. * @param parser must not be {@literal null}. */ public SpelPropertyComparator(String path, SpelExpressionParser parser) { Assert.hasText(path, "Path must not be null or empty"); Assert.notNull(parser, "SpelExpressionParser must not be null"); this.path = path; this.parser = parser; } /** * Sort {@literal ascending}. * * @return */ @Contract("-> this") public SpelPropertyComparator<@Nullable T> asc() { this.asc = true; return this; } /** * Sort {@literal descending}. * * @return */ @Contract("-> this") public SpelPropertyComparator<@Nullable T> desc() { this.asc = false; return this; } /** * Sort {@literal null} values first. * * @return */ @Contract("-> this") public SpelPropertyComparator<@Nullable T> nullsFirst() { this.nullsFirst = true; return this; } /** * Sort {@literal null} values last. * * @return */ @Contract("-> this") public SpelPropertyComparator<@Nullable T> nullsLast() { this.nullsFirst = false; return this; } /** * Parse values to {@link SpelExpression} * * @return */ protected SpelExpression getExpression() { if (this.expression == null) { this.expression = parser.parseRaw(buildExpressionForPath()); } return this.expression; } /** * Create the expression raw value. * * @return */ protected String buildExpressionForPath() { return String.format("#comparator.compare(#arg1?.%s,#arg2?.%s)", path.replace(".", "?."), path.replace(".", "?.")); } @Override public int compare(@Nullable T arg1, @Nullable T arg2) { SpelExpression expressionToUse = getExpression(); SimpleEvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build(); ctx.setVariable("comparator", nullsFirst ? NULLS_FIRST : NULLS_LAST); ctx.setVariable("arg1", arg1); ctx.setVariable("arg2", arg2); expressionToUse.setEvaluationContext(ctx); Integer value = expressionToUse.getValue(Integer.class); return (value != null ? value : 0) * (asc ? 1 : -1); } /** * Get dot path to property. * * @return */ public String getPath() { return path; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; /** * {@link QueryEngine} implementation specific for executing {@link SpelExpression} based {@link KeyValueQuery} against * {@link KeyValueAdapter}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class SpelQueryEngine extends QueryEngine> { private static final SpelExpressionParser PARSER = new SpelExpressionParser(); /** * Creates a new {@link SpelQueryEngine}. */ public SpelQueryEngine() { this(new SpelSortAccessor(PARSER)); } /** * Creates a new query engine using provided {@link SortAccessor accessor} for sorting results. * * @since 3.1.10 */ public SpelQueryEngine(SortAccessor> sortAccessor) { super(new SpelCriteriaAccessor(PARSER), sortAccessor); } @Override public Collection execute(@Nullable SpelCriteria criteria, @Nullable Comparator sort, long offset, int rows, String keyspace) { return sortAndFilterMatchingRange(getRequiredAdapter().getAllOf(keyspace), criteria, sort, offset, rows); } @Override public long count(@Nullable SpelCriteria criteria, String keyspace) { return filterMatchingRange(IterableConverter.toList(getRequiredAdapter().getAllOf(keyspace)), criteria, -1, -1) .size(); } @SuppressWarnings({ "unchecked", "rawtypes" }) private List sortAndFilterMatchingRange(Iterable source, @Nullable SpelCriteria criteria, @Nullable Comparator sort, long offset, int rows) { List tmp = IterableConverter.toList(source); if (sort != null) { tmp.sort(sort); } return filterMatchingRange(tmp, criteria, offset, rows); } private static List filterMatchingRange(List source, @Nullable SpelCriteria criteria, long offset, int rows) { Stream stream = source.stream(); if (criteria != null) { stream = stream.filter(it -> evaluateExpression(criteria, it)); } if (offset > 0) { stream = stream.skip(offset); } if (rows > 0) { stream = stream.limit(rows); } return stream.collect(Collectors.toList()); } @SuppressWarnings("NullAway") private static boolean evaluateExpression(SpelCriteria criteria, Object candidate) { try { return criteria.getExpression().getValue(criteria.getContext(), candidate, Boolean.class); } catch (SpelEvaluationException e) { criteria.getContext().setVariable("it", candidate); return criteria.getExpression().getValue(criteria.getContext()) == null ? false : criteria.getExpression().getValue(criteria.getContext(), Boolean.class); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/SpelSortAccessor.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import java.util.Comparator; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** * {@link SortAccessor} implementation capable of creating {@link SpelPropertyComparator}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class SpelSortAccessor implements SortAccessor> { private final SpelExpressionParser parser; /** * Creates a new {@link SpelSortAccessor} given {@link SpelExpressionParser}. * * @param parser must not be {@literal null}. */ public SpelSortAccessor(SpelExpressionParser parser) { Assert.notNull(parser, "SpelExpressionParser must not be null"); this.parser = parser; } @Override public @Nullable Comparator resolve(KeyValueQuery query) { if (query.getSort().isUnsorted()) { return null; } Optional> comparator = Optional.empty(); for (Order order : query.getSort()) { SpelPropertyComparator spelSort = new SpelPropertyComparator<>(order.getProperty(), parser); if (Direction.DESC.equals(order.getDirection())) { spelSort.desc(); if (!NullHandling.NATIVE.equals(order.getNullHandling())) { spelSort = NullHandling.NULLS_FIRST.equals(order.getNullHandling()) ? spelSort.nullsFirst() : spelSort.nullsLast(); } } if (!comparator.isPresent()) { comparator = Optional.of(spelSort); } else { SpelPropertyComparator spelSortToUse = spelSort; comparator = comparator.map(it -> it.thenComparing(spelSortToUse)); } } return comparator.orElseThrow( () -> new IllegalStateException("No sort definitions have been added to this CompoundComparator to compare")); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/UncategorizedKeyValueException.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import org.springframework.dao.UncategorizedDataAccessException; /** * Normal superclass when we can't distinguish anything more specific than "something went wrong with the underlying * resource". * * @author Christoph Strobl * @author Mark Paluch */ public class UncategorizedKeyValueException extends UncategorizedDataAccessException { private static final long serialVersionUID = -8087116071859122297L; /** * Creates a new {@link UncategorizedKeyValueException}. * * @param msg the detail message. * @param cause the root cause (usually from using a underlying data access API). */ public UncategorizedKeyValueException(String msg, Throwable cause) { super(msg, cause); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/event/KeyValueEvent.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.event; import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; /** * {@link KeyValueEvent} gets published for operations executed by eg. * {@link org.springframework.data.keyvalue.core.KeyValueTemplate}. * * Use the {@link KeyValueEvent.KeyBasedEvent#getType()} to determine which event has been emitted. * * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch * @param */ public class KeyValueEvent extends ApplicationEvent { private static final long serialVersionUID = -7128527253428193044L; private final String keyspace; protected KeyValueEvent(Object source, String keyspace) { super(source); this.keyspace = keyspace; } /** * @return affected keyspace. Never {@literal null}. */ public String getKeyspace() { return keyspace; } @Override public String toString() { return "KeyValueEvent [keyspace=" + keyspace + ", source=" + getSource() + "]"; } /** * Create new {@link BeforeGetEvent}. * * @param id * @param keyspace * @param type * @return */ public static BeforeGetEvent beforeGet(Object id, String keyspace, Class type) { return new BeforeGetEvent<>(id, keyspace, type); } /** * Create new {@link AfterGetEvent}. * * @param id * @param keyspace * @param type * @param value * @return */ public static AfterGetEvent afterGet(Object id, String keyspace, Class type, @Nullable T value) { return new AfterGetEvent<>(id, keyspace, type, value); } /** * Create new {@link BeforeInsertEvent}. * * @param id * @param keyspace * @param type * @param value * @return */ public static BeforeInsertEvent beforeInsert(Object id, String keyspace, Class type, T value) { return new BeforeInsertEvent<>(id, keyspace, type, value); } /** * Create new {@link AfterInsertEvent}. * * @param id * @param keyspace * @param type * @param value * @return */ public static AfterInsertEvent afterInsert(Object id, String keyspace, Class type, T value) { return new AfterInsertEvent<>(id, keyspace, type, value); } /** * Create new {@link BeforeUpdateEvent}. * * @param id * @param keyspace * @param type * @param value * @return */ public static BeforeUpdateEvent beforeUpdate(Object id, String keyspace, Class type, T value) { return new BeforeUpdateEvent<>(id, keyspace, type, value); } /** * Create new {@link AfterUpdateEvent}. * * @param id * @param keyspace * @param type * @param actualValue * @param previousValue * @return */ public static AfterUpdateEvent afterUpdate(Object id, String keyspace, Class type, T actualValue, @Nullable Object previousValue) { return new AfterUpdateEvent<>(id, keyspace, type, actualValue, previousValue); } /** * Create new {@link BeforeDropKeySpaceEvent}. * * @param keyspace * @param type * @return */ public static BeforeDropKeySpaceEvent beforeDropKeySpace(String keyspace, Class type) { return new BeforeDropKeySpaceEvent<>(keyspace, type); } /** * Create new {@link AfterDropKeySpaceEvent}. * * @param keyspace * @param type * @return */ public static AfterDropKeySpaceEvent afterDropKeySpace(String keyspace, Class type) { return new AfterDropKeySpaceEvent<>(keyspace, type); } /** * Create new {@link BeforeDeleteEvent}. * * @param id * @param keyspace * @param type * @return */ public static BeforeDeleteEvent beforeDelete(Object id, String keyspace, Class type) { return new BeforeDeleteEvent<>(id, keyspace, type); } /** * Create new {@link AfterDeleteEvent}. * * @param id * @param keyspace * @param type * @param value * @return */ public static AfterDeleteEvent afterDelete(Object id, String keyspace, Class type, @Nullable T value) { return new AfterDeleteEvent<>(id, keyspace, type, value); } /** * @author Christoph Strobl * @param */ @SuppressWarnings("serial") abstract static class KeyBasedEvent extends KeyValueEvent { private Object key; private Class type; KeyBasedEvent(Object key, String keyspace, Class type) { super(type, keyspace); this.key = key; this.type = type; } public Object getKey() { return key; } @Override public Object getSource() { return getKey(); } /** * Get the type of the element the {@link KeyValueEvent} refers to. * * @return */ public Class getType() { return type; } } /** * @author Christoph Strobl * @param */ @SuppressWarnings("serial") abstract static class KeyBasedEventWithPayload extends KeyBasedEvent { private final @Nullable T payload; KeyBasedEventWithPayload(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type); this.payload = payload; } /** * Get the value of the element the {@link KeyValueEvent} refers to. Can be {@literal null}. * * @return */ public @Nullable T getPayload() { return payload; } } /** * {@link KeyValueEvent} raised before loading an object by its {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class BeforeGetEvent extends KeyBasedEvent { protected BeforeGetEvent(Object key, String keyspace, Class type) { super(key, keyspace, type); } } /** * {@link KeyValueEvent} after loading an object by its {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class AfterGetEvent extends KeyBasedEventWithPayload { protected AfterGetEvent(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type, payload); } } /** * {@link KeyValueEvent} before inserting an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class BeforeInsertEvent extends KeyBasedEventWithPayload { public BeforeInsertEvent(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type, payload); } } /** * {@link KeyValueEvent} after inserting an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class AfterInsertEvent extends KeyBasedEventWithPayload { public AfterInsertEvent(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type, payload); } } /** * {@link KeyValueEvent} before updating an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class BeforeUpdateEvent extends KeyBasedEventWithPayload { public BeforeUpdateEvent(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type, payload); } } /** * {@link KeyValueEvent} after updating an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class AfterUpdateEvent extends KeyBasedEventWithPayload { private final @Nullable Object existing; public AfterUpdateEvent(Object key, String keyspace, Class type, T payload, @Nullable Object existing) { super(key, keyspace, type, payload); this.existing = existing; } /** * Get the value before update. Can be {@literal null}. * * @return */ public @Nullable Object before() { return existing; } /** * Get the current value. * * @return can be {@literal null}. */ public @Nullable T after() { return getPayload(); } } /** * {@link KeyValueEvent} before removing an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class BeforeDeleteEvent extends KeyBasedEvent { public BeforeDeleteEvent(Object key, String keyspace, Class type) { super(key, keyspace, type); } } /** * {@link KeyValueEvent} after removing an object by with a given {@literal key}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class AfterDeleteEvent extends KeyBasedEventWithPayload { public AfterDeleteEvent(Object key, String keyspace, Class type, @Nullable T payload) { super(key, keyspace, type, payload); } } /** * {@link KeyValueEvent} before removing all elements in a given {@literal keyspace}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class BeforeDropKeySpaceEvent extends KeyValueEvent { public BeforeDropKeySpaceEvent(String keyspace, Class type) { super(type, keyspace); } @Override @SuppressWarnings("unchecked") public Class getSource() { return (Class) super.getSource(); } } /** * {@link KeyValueEvent} after removing all elements in a given {@literal keyspace}. * * @author Christoph Strobl * @param */ @SuppressWarnings("serial") public static class AfterDropKeySpaceEvent extends KeyValueEvent { public AfterDropKeySpaceEvent(String keyspace, Class type) { super(type, keyspace); } @Override @SuppressWarnings("unchecked") public Class getSource() { return (Class) super.getSource(); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/event/package-info.java ================================================ /** * Support classes for key-value events, like standard persistence lifecycle events. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.core.event; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/AnnotationBasedKeySpaceResolver.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.annotation.KeySpace; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * {@link AnnotationBasedKeySpaceResolver} looks up {@link Persistent} and checks for presence of either meta or direct * usage of {@link KeySpace}. If non found it will default the keyspace to {@link Class#getName()}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public enum AnnotationBasedKeySpaceResolver implements KeySpaceResolver { INSTANCE; @Override public @Nullable String resolveKeySpace(Class type) { Assert.notNull(type, "Type for keyspace for null"); Class userClass = ClassUtils.getUserClass(type); Object keySpace = getKeySpace(userClass); return keySpace != null ? keySpace.toString() : null; } private static @Nullable Object getKeySpace(Class type) { MergedAnnotation annotation = MergedAnnotations .from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(KeySpace.class); if (annotation.isPresent()) { return annotation.getValue("value").orElse(null); } return null; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/BasicKeyValuePersistentEntity.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.jspecify.annotations.Nullable; import org.springframework.data.core.TypeInformation; import org.springframework.data.expression.ValueExpression; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * {@link KeyValuePersistentEntity} implementation that adds specific meta-data such as the {@literal keySpace}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch * @param */ public class BasicKeyValuePersistentEntity> extends BasicPersistentEntity implements KeyValuePersistentEntity { private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new); private final @Nullable ValueExpression keyspaceExpression; private final String keyspace; /** * @param information must not be {@literal null}. * @since 3.1 */ public BasicKeyValuePersistentEntity(TypeInformation information) { this(information, (String) null); } /** * @param information must not be {@literal null}. * @param keySpaceResolver can be {@literal null}. */ public BasicKeyValuePersistentEntity(TypeInformation information, @Nullable KeySpaceResolver keySpaceResolver) { this(information, keySpaceResolver != null ? keySpaceResolver.resolveKeySpace(information.getType()) : null); } private BasicKeyValuePersistentEntity(TypeInformation information, @Nullable String keyspace) { super(information); if (StringUtils.hasText(keyspace)) { this.keyspace = keyspace; this.keyspaceExpression = null; } else { Class type = information.getType(); String detectedKeyspace = AnnotationBasedKeySpaceResolver.INSTANCE.resolveKeySpace(type); if (StringUtils.hasText(detectedKeyspace)) { this.keyspace = detectedKeyspace; this.keyspaceExpression = detectExpression(detectedKeyspace); } else { this.keyspace = ClassNameKeySpaceResolver.INSTANCE.resolveKeySpace(type); this.keyspaceExpression = null; } } } /** * Returns a SpEL {@link Expression} if the given {@link String} is actually an expression that does not evaluate to a * {@link LiteralExpression} (indicating that no subsequent evaluation is necessary). * * @param potentialExpression must not be {@literal null} * @return the parsed {@link Expression} or {@literal null}. */ private static @Nullable ValueExpression detectExpression(String potentialExpression) { ValueExpression expression = PARSER.parse(potentialExpression); return expression.isLiteral() ? null : expression; } @Override public String getKeySpace() { return keyspaceExpression == null // ? keyspace // : ObjectUtils.nullSafeToString(keyspaceExpression.evaluate(getValueEvaluationContext(null))); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/ClassNameKeySpaceResolver.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * Most trivial implementation of {@link KeySpaceResolver} returning the {@link Class#getName()}. * * @author Christoph Strobl * @author Oliver Gierke */ public enum ClassNameKeySpaceResolver implements KeySpaceResolver { INSTANCE; @Override public String resolveKeySpace(Class type) { Assert.notNull(type, "Type must not be null"); return ClassUtils.getUserClass(type).getName(); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/KeySpaceResolver.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.jspecify.annotations.Nullable; /** * {@link KeySpaceResolver} determines the {@literal keyspace} a given type is assigned to. A keyspace in this context * is a specific region/collection/grouping of elements sharing a common keyrange. * * @author Christoph Strobl * @author Mark Paluch */ public interface KeySpaceResolver { /** * Determine the {@literal keySpace} to use for a given type. * * @param type must not be {@literal null}. * @return */ @Nullable String resolveKeySpace(Class type); } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/KeyValuePersistentEntity.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.MutablePersistentEntity; /** * KeyValue-specific extension of {@link PersistentEntity} declaring a {@literal keySpace}. * * @author Christoph Strobl * @author Mark Paluch * @param */ public interface KeyValuePersistentEntity> extends MutablePersistentEntity { /** * Get the {@literal keySpace} a given entity assigns to. * * @return can be {@literal null}. */ @Nullable String getKeySpace(); /** * Returns the required {@literal keySpace} or throws an {@link IllegalStateException} if the {@literal keySpace} is * not defined. * * @return the {@literal keySpace} property of this {@link PersistentEntity}. * @throws IllegalStateException if the {@literal keySpace} is not defined. * @since 4.0 */ default String getRequiredKeySpace() { String keySpace = getKeySpace(); if (keySpace != null) { return keySpace; } throw new IllegalStateException(String.format("Required keySpace not defined for %s", getType())); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/KeyValuePersistentProperty.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; /** * Most trivial implementation of {@link PersistentProperty}. * * @author Christoph Strobl * @author Mark Paluch */ public class KeyValuePersistentProperty

> extends AnnotationBasedPersistentProperty

{ public KeyValuePersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { super(property, owner, simpleTypeHolder); } @Override @SuppressWarnings("unchecked") protected Association

createAssociation() { return new Association<>((P) this, null); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/PrefixKeyspaceResolver.java ================================================ /* * Copyright 2022-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import org.springframework.util.Assert; /** * {@link KeySpaceResolver} prefixing the {@literal keyspace} with a static prefix after determining the keyspace from a * delegate {@link KeySpaceResolver}. * * @author Mark Paluch * @since 3.1 */ public class PrefixKeyspaceResolver implements KeySpaceResolver { private final String prefix; private final KeySpaceResolver delegate; public PrefixKeyspaceResolver(String prefix, KeySpaceResolver delegate) { Assert.notNull(prefix, "Prefix must not be null"); Assert.notNull(delegate, "Delegate KeySpaceResolver must not be null"); this.prefix = prefix; this.delegate = delegate; } @Override public String resolveKeySpace(Class type) { return prefix + delegate.resolveKeySpace(type); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/context/KeyValueMappingContext.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping.context; import java.util.Collections; import org.jspecify.annotations.Nullable; import org.springframework.data.core.TypeInformation; import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; /** * Default implementation of a {@link MappingContext} using {@link KeyValuePersistentEntity} and * {@link KeyValuePersistentProperty} as primary abstractions. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class KeyValueMappingContext, P extends KeyValuePersistentProperty

> extends AbstractMappingContext { private @Nullable KeySpaceResolver keySpaceResolver; public KeyValueMappingContext() { setSimpleTypeHolder(new KeyValueSimpleTypeHolder()); } /** * Configures the {@link KeySpaceResolver} to be used if not explicit key space is annotated to the domain type. * * @param fallbackKeySpaceResolver can be {@literal null}. * @deprecated since 3.1, use {@link KeySpaceResolver} instead. */ @Deprecated(since = "3.1") public void setFallbackKeySpaceResolver(KeySpaceResolver fallbackKeySpaceResolver) { setKeySpaceResolver(fallbackKeySpaceResolver); } /** * Configures the {@link KeySpaceResolver} to be used. Configuring a {@link KeySpaceResolver} disables SpEL evaluation * abilities. * * @param keySpaceResolver can be {@literal null}. * @since 3.1 */ public void setKeySpaceResolver(KeySpaceResolver keySpaceResolver) { this.keySpaceResolver = keySpaceResolver; } /** * @return the current {@link KeySpaceResolver}. Can be {@literal null}. * @since 3.1 */ public @Nullable KeySpaceResolver getKeySpaceResolver() { return keySpaceResolver; } @Override @SuppressWarnings("unchecked") protected E createPersistentEntity(TypeInformation typeInformation) { return (E) new BasicKeyValuePersistentEntity(typeInformation, getKeySpaceResolver()); } @Override @SuppressWarnings("unchecked") protected P createPersistentProperty(Property property, E owner, SimpleTypeHolder simpleTypeHolder) { return (P) new KeyValuePersistentProperty<>(property, owner, simpleTypeHolder); } /** * @since 2.5.1 */ private static class KeyValueSimpleTypeHolder extends SimpleTypeHolder { public KeyValueSimpleTypeHolder() { super(Collections.emptySet(), true); } @Override public boolean isSimpleType(Class type) { if (type.getName().startsWith("java.") || type.getName().startsWith("javax.")) { return true; } return super.isSimpleType(type); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/context/package-info.java ================================================ /** * Infrastructure for the Key-Value mapping context. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.core.mapping.context; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/mapping/package-info.java ================================================ /** * Infrastructure for the Key-Value mapping subsystem and keyspace resolution. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.core.mapping; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/package-info.java ================================================ /** * Core key/value implementation. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.core; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/query/KeyValueQuery.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.query; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.lang.Contract; import org.springframework.util.Assert; /** * @author Christoph Strobl * @author Mark Paluch * @author Marcel Overdijk * @param Criteria type */ public class KeyValueQuery { private Sort sort = Sort.unsorted(); private long offset = -1; private int rows = -1; private final @Nullable T criteria; /** * Creates new instance of {@link KeyValueQuery}. */ public KeyValueQuery() { this((T) null); } /** * Creates new instance of {@link KeyValueQuery} with given criteria. * * @param criteria can be {@literal null}. */ public KeyValueQuery(@Nullable T criteria) { this.criteria = criteria; } /** * Creates new instance of {@link KeyValueQuery} with given criteria and {@link Sort}. * * @param criteria can be {@literal null}. * @param sort must not be {@literal null}. * @since 2.4 */ public KeyValueQuery(@Nullable T criteria, Sort sort) { this.criteria = criteria; setSort(sort); } /** * Creates new instance of {@link KeyValueQuery} with given {@link Sort}. * * @param sort must not be {@literal null}. */ public KeyValueQuery(Sort sort) { this(); setSort(sort); } /** * Get the criteria object. * * @return * @since 2.0 */ public @Nullable T getCriteria() { return criteria; } /** * Get {@link Sort}. * * @return */ public Sort getSort() { return sort; } /** * Number of elements to skip. * * @return negative value if not set. */ public long getOffset() { return this.offset; } /** * Number of elements to read. * * @return negative value if not set. */ public int getRows() { return this.rows; } /** * Set the number of elements to skip. * * @param offset use negative value for none. */ public void setOffset(long offset) { this.offset = offset; } /** * Set the number of elements to read. * * @param rows use negative value for all. */ public void setRows(int rows) { this.rows = rows; } /** * Set {@link Sort} to be applied. * * @param sort */ public void setSort(Sort sort) { Assert.notNull(sort, "Sort must not be null"); this.sort = sort; } /** * Add given {@link Sort}. * * @param sort must not be {@literal null}. * @return */ @Contract("_ -> this") public KeyValueQuery orderBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); if (this.sort.isSorted()) { this.sort = this.sort.and(sort); } else { this.sort = sort; } return this; } /** * @see KeyValueQuery#setOffset(long) * @param offset * @return */ @Contract("_ -> this") public KeyValueQuery skip(long offset) { setOffset(offset); return this; } /** * @see KeyValueQuery#setRows(int) * @param rows * @return */ @Contract("_ -> this") public KeyValueQuery limit(int rows) { setRows(rows); return this; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/core/query/package-info.java ================================================ /** * Key/value specific query and abstractions. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.core.query; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/KeyValueRepository.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.ListPagingAndSortingRepository; /** * @author Christoph Strobl * @param * @param */ public interface KeyValueRepository extends ListCrudRepository, ListPagingAndSortingRepository { } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.config; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.SpelQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; /** * {@link RepositoryConfigurationExtension} for {@link KeyValueRepository}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public abstract class KeyValueRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { protected static final String MAPPING_CONTEXT_BEAN_NAME = "keyValueMappingContext"; protected static final String KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE = "keyValueTemplateRef"; @Override public String getRepositoryFactoryBeanClassName() { return KeyValueRepositoryFactoryBean.class.getName(); } @Override public String getModuleName() { return "KeyValue"; } @Override protected String getModulePrefix() { return getModuleIdentifier(); } @Override protected Collection> getIdentifyingTypes() { return Collections.singleton(KeyValueRepository.class); } @Override public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { AnnotationAttributes attributes = config.getAttributes(); builder.addPropertyReference("keyValueOperations", attributes.getString(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE)); builder.addPropertyValue("queryCreator", getQueryCreatorType(config)); builder.addPropertyValue("queryType", getQueryType(config)); builder.addPropertyReference("mappingContext", getMappingContextBeanRef()); } /** * Detects the query creator type to be used for the factory to set. Will lookup a {@link QueryCreatorType} annotation * on the {@code @Enable}-annotation or use {@link SpelQueryCreator} if not found. * * @param config must not be {@literal null}. * @return */ private static Class getQueryCreatorType(AnnotationRepositoryConfigurationSource config) { AnnotationMetadata amd = (AnnotationMetadata) config.getSource(); MergedAnnotation queryCreator = amd.getAnnotations().get(QueryCreatorType.class); Class queryCreatorType = queryCreator.isPresent() ? queryCreator.getClass("value") : Class.class; if (queryCreatorType == Class.class) { return SpelQueryCreator.class; } return queryCreatorType; } /** * Detects the query creator type to be used for the factory to set. Will lookup a {@link QueryCreatorType} annotation * on the {@code @Enable}-annotation or use {@link SpelQueryCreator} if not found. * * @param config * @return */ private static Class getQueryType(AnnotationRepositoryConfigurationSource config) { AnnotationMetadata metadata = config.getEnableAnnotationMetadata(); Map queryCreatorAnnotationAttributes = metadata .getAnnotationAttributes(QueryCreatorType.class.getName()); if (queryCreatorAnnotationAttributes == null) { return KeyValuePartTreeQuery.class; } AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes(queryCreatorAnnotationAttributes); return queryCreatorAttributes.getClass("repositoryQueryType"); } @Override public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { super.registerBeansForRoot(registry, configurationSource); registerIfNotAlreadyRegistered(() -> { RootBeanDefinition mappingContext = new RootBeanDefinition(KeyValueMappingContext.class); mappingContext.setSource(configurationSource.getSource()); return mappingContext; }, registry, getMappingContextBeanRef(), configurationSource); Optional keyValueTemplateName = configurationSource.getAttribute(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE); // No custom template reference configured and no matching bean definition found if (keyValueTemplateName.isPresent() && getDefaultKeyValueTemplateRef().equals(keyValueTemplateName.get()) && !registry.containsBeanDefinition(keyValueTemplateName.get())) { AbstractBeanDefinition beanDefinition = getDefaultKeyValueTemplateBeanDefinition(configurationSource); if (beanDefinition != null && configurationSource.getSource() != null) { registerIfNotAlreadyRegistered(() -> beanDefinition, registry, keyValueTemplateName.get(), configurationSource.getSource()); } } } /** * Get the default {@link RootBeanDefinition} for {@link org.springframework.data.keyvalue.core.KeyValueTemplate}. * * @return {@literal null} to explicitly not register a template. * @see #getDefaultKeyValueTemplateRef() */ protected @Nullable AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( RepositoryConfigurationSource configurationSource) { return null; } /** * Returns the {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean name to potentially register a * default {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean if no bean is registered with the * returned name. * * @return the default {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean name. Never * {@literal null}. * @see #getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource) */ protected abstract String getDefaultKeyValueTemplateRef(); /** * Returns the {@link org.springframework.data.mapping.context.MappingContext} bean name to potentially register a * default mapping context bean if no bean is registered with the returned name. Defaults to * {@link MAPPING_CONTEXT_BEAN_NAME}. * * @return the {@link org.springframework.data.mapping.context.MappingContext} bean name. Never {@literal null}. * @since 2.0 */ protected String getMappingContextBeanRef() { return MAPPING_CONTEXT_BEAN_NAME; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/config/QueryCreatorType.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.config; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.AbstractQueryCreator; /** * Annotation to customize the query creator type to be used for a specific store. * * @author Oliver Gierke * @author Christoph Strobl */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface QueryCreatorType { Class> value(); /** * The {@link RepositoryQuery} type to be created by the {@link QueryCreatorType#value()}. * * @return * @since 1.1 */ Class repositoryQueryType() default KeyValuePartTreeQuery.class; } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/config/package-info.java ================================================ /** * Support infrastructure for the configuration of key/value specific repositories. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.repository.config; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/package-info.java ================================================ /** * Key/value specific repository implementation. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.repository; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/query/CachingKeyValuePartTreeQuery.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.PartTree; /** * {@link KeyValuePartTreeQuery} implementation deriving queries from {@link PartTree} using a predefined * {@link AbstractQueryCreator} that caches the once created query. * * @author Christoph Strobl * @author Mark Paluch * @since 1.1 */ public class CachingKeyValuePartTreeQuery extends KeyValuePartTreeQuery { private @Nullable KeyValueQuery cachedQuery; public CachingKeyValuePartTreeQuery(QueryMethod queryMethod, ValueExpressionDelegate valueExpressionDelegate, KeyValueOperations keyValueOperations, Class> queryCreator) { super(queryMethod, valueExpressionDelegate, keyValueOperations, queryCreator); } protected KeyValueQuery prepareQuery(Object[] parameters) { if (cachedQuery == null) { cachedQuery = super.prepareQuery(parameters); } return prepareQuery(cachedQuery, parameters); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import java.lang.reflect.Constructor; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.keyvalue.core.IterableConverter; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.SpelCriteria; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.Lazy; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * {@link RepositoryQuery} implementation deriving queries from {@link PartTree} using a predefined * {@link AbstractQueryCreator}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class KeyValuePartTreeQuery implements RepositoryQuery { private final Lazy partTree; private final QueryMethod queryMethod; private final KeyValueOperations keyValueOperations; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryCreatorFactory, ?>> queryCreatorFactory; private final ValueEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link KeyValuePartTreeQuery} for the given {@link QueryMethod}, {@link EvaluationContextProvider}, * {@link KeyValueOperations} and query creator type. * * @param queryMethod must not be {@literal null}. * @param valueExpressionDelegate must not be {@literal null}. * @param keyValueOperations must not be {@literal null}. * @param queryCreator must not be {@literal null}. */ public KeyValuePartTreeQuery(QueryMethod queryMethod, ValueExpressionDelegate valueExpressionDelegate, KeyValueOperations keyValueOperations, Class> queryCreator) { this(queryMethod, valueExpressionDelegate, keyValueOperations, new ConstructorCachingQueryCreatorFactory(queryCreator)); } /** * Creates a new {@link KeyValuePartTreeQuery} for the given {@link QueryMethod}, {@link EvaluationContextProvider}, * {@link KeyValueOperations} using the given {@link QueryCreatorFactory} producing the {@link AbstractQueryCreator} * in charge of altering the query. * * @param queryMethod must not be {@literal null}. * @param valueExpressionDelegate must not be {@literal null}. * @param keyValueOperations must not be {@literal null}. * @param queryCreatorFactory must not be {@literal null}. * @since 2.0 */ public KeyValuePartTreeQuery(QueryMethod queryMethod, ValueExpressionDelegate valueExpressionDelegate, KeyValueOperations keyValueOperations, QueryCreatorFactory, ?>> queryCreatorFactory) { Assert.notNull(queryMethod, "Query method must not be null"); Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null"); Assert.notNull(keyValueOperations, "KeyValueOperations must not be null"); Assert.notNull(queryCreatorFactory, "QueryCreatorFactory type must not be null"); this.partTree = Lazy .of(() -> new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType())); this.queryMethod = queryMethod; this.keyValueOperations = keyValueOperations; this.valueExpressionDelegate = valueExpressionDelegate; this.queryCreatorFactory = queryCreatorFactory; this.evaluationContextProvider = valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()); } @Override public @Nullable Object execute(Object[] parameters) { ParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters); KeyValueQuery query = prepareQuery(parameters); ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor); return processor.processResult(doExecute(parameters, query)); } /** * @param parameters * @param query */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected @Nullable Object doExecute(Object[] parameters, KeyValueQuery query) { if (queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { Pageable page = (Pageable) parameters[queryMethod.getParameters().getPageableIndex()]; query.setOffset(page.getOffset()); query.setRows(page.getPageSize()); Iterable result = this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType()); long count = queryMethod.isSliceQuery() ? 0 : keyValueOperations.count(query, queryMethod.getEntityInformation().getJavaType()); return new PageImpl(IterableConverter.toList(result), page, count); } else if (queryMethod.isCollectionQuery()) { return this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType()); } else if (partTree.get().isExistsProjection()) { return keyValueOperations.exists(query, queryMethod.getEntityInformation().getJavaType()); } else if (partTree.get().isCountProjection()) { return keyValueOperations.count(query, queryMethod.getEntityInformation().getJavaType()); } else { Iterable result = this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType()); return result.iterator().hasNext() ? result.iterator().next() : null; } } protected KeyValueQuery prepareQuery(Object[] parameters) { return prepareQuery(createQuery(new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters)), parameters); } @SuppressWarnings({ "rawtypes", "unchecked" }) protected KeyValueQuery prepareQuery(KeyValueQuery instance, Object[] parameters) { ParametersParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters); Object criteria = instance.getCriteria(); if (criteria instanceof SpelCriteria || criteria instanceof SpelExpression) { SpelExpression spelExpression = getSpelExpression(criteria); ValueEvaluationContext context = this.evaluationContextProvider.getEvaluationContext(parameters); criteria = new SpelCriteria(spelExpression, context.getRequiredEvaluationContext()); } KeyValueQuery query = new KeyValueQuery(criteria); Pageable pageable = accessor.getPageable(); Sort sort = accessor.getSort(); query.setOffset(pageable.toOptional().map(Pageable::getOffset).orElse(-1L)); if (pageable.isPaged()) { query.setRows(pageable.getPageSize()); } else if (instance.getRows() >= 0) { query.setRows(instance.getRows()); } query.setSort(sort.isUnsorted() ? instance.getSort() : sort); return query; } private SpelExpression getSpelExpression(Object criteria) { if (criteria instanceof SpelExpression) { return (SpelExpression) criteria; } if (criteria instanceof SpelCriteria) { return getSpelExpression(((SpelCriteria) criteria).getExpression()); } throw new IllegalStateException(String.format("Cannot retrieve SpelExpression from %s", criteria)); } /** * Create a {@link KeyValueQuery} given {@link ParameterAccessor}. * * @param accessor must not be {@literal null}. * @return the {@link KeyValueQuery}. */ @SuppressWarnings("NullAway") public KeyValueQuery createQuery(ParameterAccessor accessor) { PartTree tree = this.partTree.get(); AbstractQueryCreator, ?> queryCreator = queryCreatorFactory.queryCreatorFor(tree, accessor); KeyValueQuery query = queryCreator.createQuery(); if (tree.isLimiting()) { query.setRows(tree.getMaxResults()); } return query; } @Override public QueryMethod getQueryMethod() { return queryMethod; } /** * Factory class for obtaining {@link AbstractQueryCreator} instances for a given {@link PartTree} and * {@link ParameterAccessor}. * * @author Christoph Strobl * @since 2.0 */ public interface QueryCreatorFactory> { T queryCreatorFor(PartTree partTree, ParameterAccessor accessor); } /** * {@link QueryCreatorFactory} implementation instantiating {@link AbstractQueryCreator} via reflection. Looks up and * caches the constructor on creation. * * @author Christoph Strobl * @since 2.0 */ private static class ConstructorCachingQueryCreatorFactory implements QueryCreatorFactory, ?>> { private final Class type; private final @Nullable Constructor> constructor; ConstructorCachingQueryCreatorFactory(Class> type) { this.type = type; this.constructor = ClassUtils.getConstructorIfAvailable(type, PartTree.class, ParameterAccessor.class); } @Override @SuppressWarnings("unchecked") public AbstractQueryCreator, ?> queryCreatorFor(PartTree partTree, ParameterAccessor accessor) { Assert.state(constructor != null, () -> String.format("No constructor (PartTree, ParameterAccessor) could be found on type %s", type)); return (AbstractQueryCreator, ?>) BeanUtils.instantiateClass(constructor, partTree, accessor); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.core.PropertyPath; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.SimplePropertyPathAccessor; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; /** * {@link AbstractQueryCreator} to create {@link Predicate}-based {@link KeyValueQuery}s. * * @author Christoph Strobl * @author Tom Van Wemmel * @author Mark Paluch * @since 3.3 */ public class PredicateQueryCreator extends AbstractQueryCreator>, Predicate> { private static final Comparator COMPARATOR = Comparator.nullsFirst(Comparator.naturalOrder()); public PredicateQueryCreator(PartTree tree, ParameterAccessor parameters) { super(tree, parameters); } @Override protected Predicate create(Part part, Iterator iterator) { PredicateBuilder builder = PredicateBuilder.propertyValueOf(part); return switch (part.getType()) { case TRUE -> builder.isTrue(); case FALSE -> builder.isFalse(); case SIMPLE_PROPERTY -> builder.isEqualTo(iterator.next()); case NEGATING_SIMPLE_PROPERTY -> builder.isEqualTo(iterator.next()).negate(); case IS_NULL -> builder.isNull(); case IS_NOT_NULL -> builder.isNotNull(); case LIKE -> builder.contains(iterator.next()); case NOT_LIKE -> builder.contains(iterator.next()).negate(); case STARTING_WITH -> builder.startsWith(iterator.next()); case AFTER, GREATER_THAN -> builder.isGreaterThan(iterator.next()); case GREATER_THAN_EQUAL -> builder.isGreaterThanEqual(iterator.next()); case BEFORE, LESS_THAN -> builder.isLessThan(iterator.next()); case LESS_THAN_EQUAL -> builder.isLessThanEqual(iterator.next()); case ENDING_WITH -> builder.endsWith(iterator.next()); case BETWEEN -> builder.isGreaterThan(iterator.next()).and(builder.isLessThan(iterator.next())); case REGEX -> builder.matches(iterator.next()); case IN -> builder.in(iterator.next()); case NOT_IN -> builder.in(iterator.next()).negate(); default -> throw new InvalidDataAccessApiUsageException(String.format("Found invalid part '%s' in query", part.getType())); }; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) protected Predicate and(Part part, Predicate base, Iterator iterator) { return base.and((Predicate) create(part, iterator)); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) protected Predicate or(Predicate base, Predicate criteria) { return base.or((Predicate) criteria); } @Override protected KeyValueQuery> complete(@Nullable Predicate criteria, Sort sort) { return criteria == null ? new KeyValueQuery<>(it -> true, sort) : new KeyValueQuery<>(criteria, sort); } static class PredicateBuilder { private final Part part; public PredicateBuilder(Part part) { this.part = part; } @SuppressWarnings("unchecked") static Comparator comparator() { return (Comparator) COMPARATOR; } static PredicateBuilder propertyValueOf(Part part) { return new PredicateBuilder(part); } public Predicate isTrue() { return new ValueComparingPredicate(part.getProperty(), true); } public Predicate isFalse() { return new ValueComparingPredicate(part.getProperty(), false); } @Contract("_ -> new") public Predicate isEqualTo(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (!ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { if (o instanceof String s1 && value instanceof String s2) { return s1.equalsIgnoreCase(s2); } } return ObjectUtils.nullSafeEquals(o, value); }); } public Predicate isNull() { return new ValueComparingPredicate(part.getProperty(), Objects::isNull); } public Predicate isNotNull() { return isNull().negate(); } @Contract("_ -> new") public Predicate isLessThan(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> comparator().compare(o, value) < 0); } @Contract("_ -> new") public Predicate isLessThanEqual(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> comparator().compare(o, value) <= 0); } @Contract("_ -> new") public Predicate isGreaterThan(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> comparator().compare(o, value) > 0); } @Contract("_ -> new") public Predicate isGreaterThanEqual(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> comparator().compare(o, value) >= 0); } @Contract("!null -> new") public Predicate matches(Pattern pattern) { return new ValueComparingPredicate(part.getProperty(), o -> { if (o == null) { return false; } return pattern.matcher(o.toString()).find(); }); } @Contract("_ -> new") public Predicate matches(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (o == null || value == null) { return ObjectUtils.nullSafeEquals(o, value); } if (value instanceof Pattern pattern) { return pattern.matcher(o.toString()).find(); } return o.toString().matches(value.toString()); }); } @Contract("!null -> new") public Predicate matches(String regex) { return matches(Pattern.compile(regex)); } @Contract("!null -> new") public Predicate in(Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (value instanceof Collection collection) { if (o instanceof Collection subSet) { return collection.containsAll(subSet); } return collection.contains(o); } if (ObjectUtils.isArray(value)) { return ObjectUtils.containsElement(ObjectUtils.toObjectArray(value), value); } return false; }); } @Contract("_ -> new") public Predicate contains(@Nullable Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (o == null) { return false; } if (o instanceof Collection collection) { return collection.contains(value); } if (ObjectUtils.isArray(o)) { return ObjectUtils.containsElement(ObjectUtils.toObjectArray(o), value); } if (o instanceof Map map) { return map.containsValue(value); } if (value == null) { return false; } String s = o.toString(); if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { return s.contains(value.toString()); } return s.toLowerCase().contains(value.toString().toLowerCase()); }); } @Contract("!null -> new") public Predicate startsWith(Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (!(o instanceof String s)) { return false; } if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { return s.startsWith(value.toString()); } return s.toLowerCase().startsWith(value.toString().toLowerCase()); }); } @Contract("!null -> new") public Predicate endsWith(Object value) { return new ValueComparingPredicate(part.getProperty(), o -> { if (!(o instanceof String s)) { return false; } if (ObjectUtils.nullSafeEquals(IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { return s.endsWith(value.toString()); } return s.toLowerCase().endsWith(value.toString().toLowerCase()); }); } } static class ValueComparingPredicate implements Predicate { private final PropertyPath path; private final Function<@Nullable Object, Boolean> check; public ValueComparingPredicate(PropertyPath path, @Nullable Object expected) { this(path, (value) -> ObjectUtils.nullSafeEquals(value, expected)); } public ValueComparingPredicate(PropertyPath path, Function<@Nullable Object, Boolean> check) { this.path = path; this.check = check; } @Override public boolean test(Object o) { Object value = new SimplePropertyPathAccessor<>(o).getValue(path); return check.apply(value); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import java.util.Iterator; import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.StringUtils; /** * {@link AbstractQueryCreator} to create {@link SpelExpression}-based {@link KeyValueQuery}s. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch * @author Tom Van Wemmel */ public class SpelQueryCreator extends AbstractQueryCreator, String> { private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final SpelExpression expression; /** * Creates a new {@link SpelQueryCreator} for the given {@link PartTree} and {@link ParameterAccessor}. * * @param tree must not be {@literal null}. * @param parameters must not be {@literal null}. */ public SpelQueryCreator(PartTree tree, ParameterAccessor parameters) { super(tree, parameters); this.expression = toPredicateExpression(tree); } @Override protected String create(Part part, Iterator iterator) { return ""; } @Override protected String and(Part part, String base, Iterator iterator) { return ""; } @Override protected String or(String base, String criteria) { return ""; } @Override protected KeyValueQuery complete(@Nullable String criteria, Sort sort) { KeyValueQuery query = new KeyValueQuery<>(this.expression); if (sort.isSorted()) { query.orderBy(sort); } return query; } protected SpelExpression toPredicateExpression(PartTree tree) { int parameterIndex = 0; StringBuilder sb = new StringBuilder(); for (Iterator orPartIter = tree.iterator(); orPartIter.hasNext();) { int partCnt = 0; StringBuilder partBuilder = new StringBuilder(); OrPart orPart = orPartIter.next(); for (Iterator partIter = orPart.iterator(); partIter.hasNext();) { Part part = partIter.next(); if (!requiresInverseLookup(part)) { partBuilder.append("#it?."); partBuilder.append(part.getProperty().toDotPath().replace(".", "?.")); } // TODO: check if we can have caseinsensitive search if (!part.shouldIgnoreCase().equals(IgnoreCaseType.NEVER)) { throw new InvalidDataAccessApiUsageException("Ignore case not supported"); } switch (part.getType()) { case TRUE: partBuilder.append("?.equals(true)"); break; case FALSE: partBuilder.append("?.equals(false)"); break; case SIMPLE_PROPERTY: case NEGATING_SIMPLE_PROPERTY: partBuilder.append("?.equals(").append("[").append(parameterIndex++).append("])"); if (part.getType() == Type.NEGATING_SIMPLE_PROPERTY) { partBuilder.append(" == false"); } break; case IS_NULL: partBuilder.append(" == null"); break; case IS_NOT_NULL: partBuilder.append(" != null"); break; case LIKE: case NOT_LIKE: partBuilder.append("?.contains(").append("[").append(parameterIndex++).append("])"); if (part.getType() == Type.NOT_LIKE) { partBuilder.append(" == false"); } break; case STARTING_WITH: partBuilder.append("?.startsWith(").append("[").append(parameterIndex++).append("])"); break; case AFTER: case GREATER_THAN: partBuilder.append(">").append("[").append(parameterIndex++).append("]"); break; case GREATER_THAN_EQUAL: partBuilder.append(">=").append("[").append(parameterIndex++).append("]"); break; case BEFORE: case LESS_THAN: partBuilder.append("<").append("[").append(parameterIndex++).append("]"); break; case LESS_THAN_EQUAL: partBuilder.append("<=").append("[").append(parameterIndex++).append("]"); break; case ENDING_WITH: partBuilder.append("?.endsWith(").append("[").append(parameterIndex++).append("])"); break; case BETWEEN: int index = partBuilder.lastIndexOf("#it?."); partBuilder.insert(index, "("); partBuilder.append(">").append("[").append(parameterIndex++).append("]"); partBuilder.append("&&"); partBuilder.append("#it?."); partBuilder.append(part.getProperty().toDotPath().replace(".", "?.")); partBuilder.append("<").append("[").append(parameterIndex++).append("]"); partBuilder.append(")"); break; case REGEX: partBuilder.append(" matches ").append("[").append(parameterIndex++).append("]"); break; case NOT_IN: case IN: partBuilder.append("[").append(parameterIndex++).append("].contains("); partBuilder.append("#it?."); partBuilder.append(part.getProperty().toDotPath().replace(".", "?.")); partBuilder.append(")"); if (part.getType() == Type.NOT_IN) { partBuilder.append(" == false"); } break; case CONTAINING: case NOT_CONTAINING: case EXISTS: default: throw new InvalidDataAccessApiUsageException("Found invalid part '%s' in query".formatted(part.getType())); } if (partIter.hasNext()) { partBuilder.append(" && "); } partCnt++; } if (partCnt > 1) { sb.append("(").append(partBuilder).append(")"); } else { sb.append(partBuilder); } if (orPartIter.hasNext()) { sb.append(" || "); } } return StringUtils.hasText(sb) ? PARSER.parseRaw(sb.toString()) : PARSER.parseRaw("true"); } private static boolean requiresInverseLookup(Part part) { return part.getType() == Type.IN || part.getType() == Type.NOT_IN; } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/query/package-info.java ================================================ /** * Query derivation mechanism for key/value specific repositories providing a generic SpEL based implementation. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.repository.query; ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/KeyValueQuerydslUtils.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import java.util.ArrayList; import java.util.List; import org.springframework.data.core.PropertyPath; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.querydsl.QSort; import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.querydsl.core.types.Expression; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.OrderSpecifier.NullHandling; import com.querydsl.core.types.Path; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.PathBuilder; /** * Utilities for Querydsl usage. * * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke * @author Mark Paluch */ abstract class KeyValueQuerydslUtils { private KeyValueQuerydslUtils() { // prevent instantiation } /** * Transforms a plain {@link Order} into a QueryDsl specific {@link OrderSpecifier}. * * @param sort must not be {@literal null}. * @param builder must not be {@literal null}. * @return empty {@code OrderSpecifier[]} when sort is {@literal null}. */ @Contract("!null, !null -> new") static OrderSpecifier[] toOrderSpecifier(Sort sort, PathBuilder builder) { Assert.notNull(sort, "Sort must not be null"); Assert.notNull(builder, "Builder must not be null"); List> specifiers = null; if (sort instanceof QSort) { specifiers = ((QSort) sort).getOrderSpecifiers(); } else { specifiers = new ArrayList<>(); for (Order order : sort) { specifiers.add(toOrderSpecifier(order, builder)); } } return specifiers.toArray(new OrderSpecifier[specifiers.size()]); } @SuppressWarnings({ "rawtypes", "unchecked" }) private static OrderSpecifier toOrderSpecifier(Order order, PathBuilder builder) { return new OrderSpecifier( order.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC, buildOrderPropertyPathFrom(order, builder), toQueryDslNullHandling(order.getNullHandling())); } /** * Creates an {@link Expression} for the given {@link Order} property. * * @param order must not be {@literal null}. * @param builder must not be {@literal null}. * @return */ private static Expression buildOrderPropertyPathFrom(Order order, PathBuilder builder) { Assert.notNull(order, "Order must not be null"); Assert.notNull(builder, "Builder must not be null"); PropertyPath path = PropertyPath.from(order.getProperty(), builder.getType()); Expression sortPropertyExpression = builder; while (path != null) { if (!path.hasNext() && order.isIgnoreCase()) { // if order is ignore-case we have to treat the last path segment as a String. sortPropertyExpression = Expressions.stringPath((Path) sortPropertyExpression, path.getSegment()).lower(); } else { sortPropertyExpression = Expressions.path(path.getType(), (Path) sortPropertyExpression, path.getSegment()); } path = path.next(); } return sortPropertyExpression; } /** * Converts the given {@link org.springframework.data.domain.Sort.NullHandling} to the appropriate Querydsl * {@link NullHandling}. * * @param nullHandling must not be {@literal null}. * @return */ private static NullHandling toQueryDslNullHandling(org.springframework.data.domain.Sort.NullHandling nullHandling) { Assert.notNull(nullHandling, "NullHandling must not be null"); switch (nullHandling) { case NULLS_FIRST: return NullHandling.NullsFirst; case NULLS_LAST: return NullHandling.NullsLast; case NATIVE: default: return NullHandling.Default; } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/KeyValueRepositoryFactory.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import static org.springframework.data.querydsl.QuerydslUtils.*; import static org.springframework.data.repository.core.support.RepositoryComposition.*; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * {@link RepositoryFactorySupport} specific of handing * {@link org.springframework.data.keyvalue.repository.KeyValueRepository}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class KeyValueRepositoryFactory extends RepositoryFactorySupport { private static final Class DEFAULT_QUERY_CREATOR = PredicateQueryCreator.class; private final KeyValueOperations keyValueOperations; private final MappingContext context; private final Class> queryCreator; private final Class repositoryQueryType; /** * Creates a new {@link KeyValueRepositoryFactory} for the given {@link KeyValueOperations}. * * @param keyValueOperations must not be {@literal null}. */ public KeyValueRepositoryFactory(KeyValueOperations keyValueOperations) { this(keyValueOperations, DEFAULT_QUERY_CREATOR); } /** * Creates a new {@link KeyValueRepositoryFactory} for the given {@link KeyValueOperations} and * {@link AbstractQueryCreator}-type. * * @param keyValueOperations must not be {@literal null}. * @param queryCreator must not be {@literal null}. */ public KeyValueRepositoryFactory(KeyValueOperations keyValueOperations, Class> queryCreator) { this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class); } /** * Creates a new {@link KeyValueRepositoryFactory} for the given {@link KeyValueOperations} and * {@link AbstractQueryCreator}-type. * * @param keyValueOperations must not be {@literal null}. * @param queryCreator must not be {@literal null}. * @param repositoryQueryType must not be {@literal null}. * @since 1.1 */ public KeyValueRepositoryFactory(KeyValueOperations keyValueOperations, Class> queryCreator, Class repositoryQueryType) { Assert.notNull(keyValueOperations, "KeyValueOperations must not be null"); Assert.notNull(queryCreator, "Query creator type must not be null"); Assert.notNull(repositoryQueryType, "RepositoryQueryType type must not be null"); this.queryCreator = queryCreator; this.keyValueOperations = keyValueOperations; this.context = keyValueOperations.getMappingContext(); this.repositoryQueryType = repositoryQueryType; } @Override @SuppressWarnings("unchecked") public EntityInformation getEntityInformation(Class domainClass) { PersistentEntity entity = (PersistentEntity) context.getRequiredPersistentEntity(domainClass); return new PersistentEntityInformation<>(entity); } @Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { EntityInformation entityInformation = getEntityInformation(repositoryInformation.getDomainType()); return super.getTargetRepositoryViaReflection(repositoryInformation, entityInformation, keyValueOperations); } @Override protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleKeyValueRepository.class; } @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { return getRepositoryFragments(metadata, keyValueOperations); } /** * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Key-Value-specific extensions. * Typically adds a {@link QuerydslKeyValuePredicateExecutor} if the repository interface uses Querydsl. *

* Can be overridden by subclasses to customize {@link RepositoryFragments}. * * @param metadata repository metadata. * @param operations the KeyValue operations manager. * @return * @since 2.6 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, KeyValueOperations operations) { if (isQueryDslRepository(metadata.getRepositoryInterface())) { if (metadata.isReactiveRepository()) { throw new InvalidDataAccessApiUsageException( "Cannot combine Querydsl and reactive repository support in a single interface"); } return RepositoryFragments .just(new QuerydslKeyValuePredicateExecutor<>(getEntityInformation(metadata.getDomainType()), getProjectionFactory(), operations, SimpleEntityPathResolver.INSTANCE)); } return RepositoryFragments.empty(); } /** * Returns whether the given repository interface requires a QueryDsl specific implementation to be chosen. * * @param repositoryInterface must not be {@literal null}. * @return */ private static boolean isQueryDslRepository(Class repositoryInterface) { return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(repositoryInterface); } @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(new KeyValueQueryLookupStrategy(key, valueExpressionDelegate, this.keyValueOperations, this.queryCreator, this.repositoryQueryType)); } /** * @author Christoph Strobl * @author Oliver Gierke */ private static class KeyValueQueryLookupStrategy implements QueryLookupStrategy { private final ValueExpressionDelegate valueExpressionDelegate; private final KeyValueOperations keyValueOperations; private final Class> queryCreator; private final Class repositoryQueryType; /** * @param key * @param valueExpressionDelegate * @param keyValueOperations * @param queryCreator * @since 1.1 */ public KeyValueQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate, KeyValueOperations keyValueOperations, Class> queryCreator, Class repositoryQueryType) { Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null"); Assert.notNull(keyValueOperations, "KeyValueOperations must not be null"); Assert.notNull(queryCreator, "Query creator type must not be null"); Assert.notNull(repositoryQueryType, "RepositoryQueryType type must not be null"); this.valueExpressionDelegate = valueExpressionDelegate; this.keyValueOperations = keyValueOperations; this.queryCreator = queryCreator; this.repositoryQueryType = repositoryQueryType; } @Override @SuppressWarnings("unchecked") public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { QueryMethod queryMethod = new QueryMethod(method, metadata, factory); Constructor constructor = (Constructor) ClassUtils .getConstructorIfAvailable(this.repositoryQueryType, QueryMethod.class, ValueExpressionDelegate.class, KeyValueOperations.class, Class.class); Assert.state(constructor != null, String.format( "Constructor %s(QueryMethod, EvaluationContextProvider, KeyValueOperations, Class) not available", ClassUtils.getShortName(this.repositoryQueryType))); return BeanUtils.instantiateClass(constructor, queryMethod, valueExpressionDelegate, this.keyValueOperations, this.queryCreator); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/KeyValueRepositoryFactoryBean.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.config.QueryCreatorType; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.util.Assert; /** * {@link org.springframework.beans.factory.FactoryBean} to create {@link KeyValueRepository}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ public class KeyValueRepositoryFactoryBean, S, ID> extends RepositoryFactoryBeanSupport { private @Nullable KeyValueOperations operations; private @Nullable Class> queryCreator; private @Nullable Class repositoryQueryType; /** * Creates a new {@link KeyValueRepositoryFactoryBean} for the given repository interface. * * @param repositoryInterface must not be {@literal null}. */ public KeyValueRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } /** * Configures the {@link KeyValueOperations} to be used for the repositories. * * @param operations must not be {@literal null}. */ public void setKeyValueOperations(KeyValueOperations operations) { Assert.notNull(operations, "KeyValueOperations must not be null"); this.operations = operations; } @Override public void setMappingContext(MappingContext mappingContext) { super.setMappingContext(mappingContext); } /** * Configures the {@link QueryCreatorType} to be used. * * @param queryCreator must not be {@literal null}. */ public void setQueryCreator(Class> queryCreator) { Assert.notNull(queryCreator, "Query creator type must not be null"); this.queryCreator = queryCreator; } /** * Configures the {@link RepositoryQuery} type to be created. * * @param repositoryQueryType must not be {@literal null}. * @since 1.1 */ public void setQueryType(Class repositoryQueryType) { Assert.notNull(repositoryQueryType, "Query creator type must not be null"); this.repositoryQueryType = repositoryQueryType; } @Override protected final RepositoryFactorySupport createRepositoryFactory() { Assert.notNull(operations, "Operations must not be null"); Assert.notNull(queryCreator, "QueryCreator must not be null"); Assert.notNull(repositoryQueryType, "QueryType must not be null"); return createRepositoryFactory(operations, queryCreator, repositoryQueryType); } /** * Create the repository factory to be used to create repositories. * * @param operations will never be {@literal null}. * @param queryCreator will never be {@literal null}. * @param repositoryQueryType will never be {@literal null}. * @return must not be {@literal null}. * @since 1.1 */ protected KeyValueRepositoryFactory createRepositoryFactory(KeyValueOperations operations, Class> queryCreator, Class repositoryQueryType) { return new KeyValueRepositoryFactory(operations, queryCreator, repositoryQueryType); } @Override public void afterPropertiesSet() { Assert.notNull(operations, "KeyValueOperations must not be null"); Assert.notNull(queryCreator, "Query creator type must not be null"); Assert.notNull(repositoryQueryType, "RepositoryQueryType type type must not be null"); super.afterPropertiesSet(); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/QuerydslKeyValuePredicateExecutor.java ================================================ /* * Copyright 2021-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import static org.springframework.data.keyvalue.repository.support.KeyValueQuerydslUtils.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.convert.DtoInstantiatingConverter; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.IterableConverter; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.ListQuerydslPredicateExecutor; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.query.FluentQuery; import org.springframework.util.Assert; import com.querydsl.collections.AbstractCollQuery; import com.querydsl.collections.CollQuery; import com.querydsl.core.NonUniqueResultException; import com.querydsl.core.QueryResults; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.PathBuilder; /** * {@link QuerydslPredicateExecutor} capable of applying {@link Predicate}s using {@link CollQuery}. * * @author Mark Paluch * @since 2.6 */ public class QuerydslKeyValuePredicateExecutor implements ListQuerydslPredicateExecutor { private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE; private final MappingContext, ? extends PersistentProperty> context; private final PathBuilder builder; private final Supplier> findAll; private final EntityInformation entityInformation; private final ProjectionFactory projectionFactory; private final EntityInstantiators entityInstantiators = new EntityInstantiators(); /** * Creates a new {@link QuerydslKeyValuePredicateExecutor} for the given {@link EntityInformation}. * * @param entityInformation must not be {@literal null}. * @param operations must not be {@literal null}. */ public QuerydslKeyValuePredicateExecutor(EntityInformation entityInformation, KeyValueOperations operations) { this(entityInformation, new SpelAwareProxyProjectionFactory(), operations, DEFAULT_ENTITY_PATH_RESOLVER); } /** * Creates a new {@link QuerydslKeyValuePredicateExecutor} for the given {@link EntityInformation}, and * {@link EntityPathResolver}. * * @param entityInformation must not be {@literal null}. * @param projectionFactory must not be {@literal null}. * @param operations must not be {@literal null}. * @param resolver must not be {@literal null}. */ public QuerydslKeyValuePredicateExecutor(EntityInformation entityInformation, ProjectionFactory projectionFactory, KeyValueOperations operations, EntityPathResolver resolver) { Assert.notNull(entityInformation, "EntityInformation must not be null"); Assert.notNull(projectionFactory, "ProjectionFactory must not be null"); Assert.notNull(operations, "KeyValueOperations must not be null"); Assert.notNull(resolver, "EntityPathResolver must not be null"); this.projectionFactory = projectionFactory; this.context = operations.getMappingContext(); EntityPath path = resolver.createPath(entityInformation.getJavaType()); this.builder = new PathBuilder<>(path.getType(), path.getMetadata()); this.entityInformation = entityInformation; findAll = () -> IterableConverter.toList(operations.findAll(entityInformation.getJavaType())); } @Override public Optional findOne(Predicate predicate) { Assert.notNull(predicate, "Predicate must not be null"); try { return Optional.ofNullable(prepareQuery(predicate).fetchOne()); } catch (NonUniqueResultException o_O) { throw new IncorrectResultSizeDataAccessException("Expected one or no result but found more than one", 1, o_O); } } @Override public List findAll(Predicate predicate) { Assert.notNull(predicate, "Predicate must not be null"); return prepareQuery(predicate).fetchResults().getResults(); } @Override public List findAll(Predicate predicate, OrderSpecifier... orders) { Assert.notNull(predicate, "Predicate must not be null"); Assert.notNull(orders, "OrderSpecifiers must not be null"); AbstractCollQuery query = prepareQuery(predicate); query.orderBy(orders); return query.fetchResults().getResults(); } @Override public List findAll(Predicate predicate, Sort sort) { Assert.notNull(predicate, "Predicate must not be null"); Assert.notNull(sort, "Sort must not be null"); return findAll(predicate, toOrderSpecifier(sort, builder)); } @Override public Page findAll(Predicate predicate, Pageable pageable) { Assert.notNull(predicate, "Predicate must not be null"); Assert.notNull(pageable, "Pageable must not be null"); AbstractCollQuery query = prepareQuery(predicate); if (pageable.isPaged() || pageable.getSort().isSorted()) { query.offset(pageable.getOffset()); query.limit(pageable.getPageSize()); if (pageable.getSort().isSorted()) { query.orderBy(toOrderSpecifier(pageable.getSort(), builder)); } } return new PageImpl<>(query.fetchResults().getResults(), pageable, count(predicate)); } @Override public List findAll(OrderSpecifier... orders) { Assert.notNull(orders, "OrderSpecifiers must not be null"); if (orders.length == 0) { return findAll.get(); } AbstractCollQuery query = prepareQuery(null); query.orderBy(orders); return query.fetchResults().getResults(); } @Override public long count(Predicate predicate) { Assert.notNull(predicate, "Predicate must not be null"); return prepareQuery(predicate).fetchCount(); } @Override public boolean exists(Predicate predicate) { Assert.notNull(predicate, "Predicate must not be null"); return count(predicate) > 0; } @Override @SuppressWarnings("unchecked") public R findBy(Predicate predicate, Function, R> queryFunction) { Assert.notNull(predicate, "Predicate must not be null"); Assert.notNull(queryFunction, "Query function must not be null"); return queryFunction.apply(new FluentQuerydsl<>(predicate, (Class) entityInformation.getJavaType())); } /** * Creates executable query for given {@link Predicate}. * * @param predicate * @return */ protected AbstractCollQuery prepareQuery(@Nullable Predicate predicate) { CollQuery query = new CollQuery<>(); query.from(builder, findAll.get()); return predicate != null ? query.where(predicate) : query; } /** * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using Querydsl * {@link Predicate}. * * @author Mark Paluch * @since 2.6 */ class FluentQuerydsl implements FluentQuery.FetchableFluentQuery { private final Predicate predicate; private final Sort sort; private final Class entityType; private final Class resultType; private final List fieldsToInclude; FluentQuerydsl(Predicate predicate, Class resultType) { this(predicate, Sort.unsorted(), resultType, resultType, Collections.emptyList()); } public FluentQuerydsl(Predicate predicate, Sort sort, Class entityType, Class resultType, List fieldsToInclude) { this.predicate = predicate; this.sort = sort; this.entityType = entityType; this.resultType = resultType; this.fieldsToInclude = fieldsToInclude; } @Override public FluentQuery.FetchableFluentQuery sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); return new FluentQuerydsl<>(predicate, sort, entityType, resultType, fieldsToInclude); } @Override public FluentQuery.FetchableFluentQuery as(Class projection) { Assert.notNull(projection, "Projection target type must not be null"); return new FluentQuerydsl<>(predicate, sort, entityType, projection, fieldsToInclude); } public FluentQuery.FetchableFluentQuery project(Collection properties) { Assert.notNull(properties, "Projection properties must not be null"); return new FluentQuerydsl<>(predicate, sort, entityType, resultType, new ArrayList<>(properties)); } @Override public @Nullable R oneValue() { List results = createQuery().limit(2).fetch(); if (results.isEmpty()) { return null; } if (results.size() > 1) { throw new IncorrectResultSizeDataAccessException(1); } T one = results.get(0); return getConversionFunction().apply(one); } @Override public @Nullable R firstValue() { List results = createQuery().limit(1).fetch(); if (results.isEmpty()) { return null; } T one = results.get(0); return getConversionFunction().apply(one); } @Override public List all() { List results = createQuery().fetch(); return mapResults(results); } @Override public Page page(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be null"); AbstractCollQuery query = createQuery(); if (pageable.isPaged() || pageable.getSort().isSorted()) { query.offset(pageable.getOffset()); query.limit(pageable.getPageSize()); if (pageable.getSort().isSorted()) { query.orderBy(toOrderSpecifier(pageable.getSort(), builder)); } } QueryResults results = query.limit(pageable.getPageSize()).offset(pageable.getOffset()).fetchResults(); return new PageImpl<>(mapResults(results.getResults()), pageable, results.getTotal()); } @Override public Stream stream() { return createQuery().stream().map(getConversionFunction()); } @Override public long count() { return createQuery().fetchCount(); } @Override public boolean exists() { return count() > 0; } private AbstractCollQuery createQuery() { AbstractCollQuery query = prepareQuery(predicate); if (sort.isSorted()) { query.orderBy(toOrderSpecifier(sort, builder)); } return query; } @SuppressWarnings("unchecked") private List mapResults(List results) { if (entityType == resultType) { return (List) results; } List mapped = new ArrayList<>(results.size()); Function converter = getConversionFunction(); for (T result : results) { mapped.add(converter.apply(result)); } return mapped; } @SuppressWarnings("unchecked") private

Function getConversionFunction(Class inputType, Class

targetType) { if (targetType.isAssignableFrom(inputType)) { return (Function) Function.identity(); } if (targetType.isInterface()) { return o -> projectionFactory.createProjection(targetType, o); } DtoInstantiatingConverter converter = new DtoInstantiatingConverter(targetType, context, entityInstantiators); return o -> (P) converter.convert(o); } private Function getConversionFunction() { return getConversionFunction(entityType, resultType); } } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/QuerydslKeyValueRepository.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import java.util.Optional; import java.util.function.Function; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.query.FluentQuery; import org.springframework.util.Assert; import com.querydsl.collections.CollQuery; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; /** * {@link KeyValueRepository} implementation capable of executing {@link Predicate}s using {@link CollQuery}. * * @author Christoph Strobl * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch * @param the domain type to manage * @param the identifier type of the domain type * @deprecated since 2.6, use {@link QuerydslKeyValuePredicateExecutor} instead. */ @Deprecated public class QuerydslKeyValueRepository extends SimpleKeyValueRepository implements QuerydslPredicateExecutor { private final QuerydslKeyValuePredicateExecutor executor; /** * Creates a new {@link QuerydslKeyValueRepository} for the given {@link EntityInformation} and * {@link KeyValueOperations}. * * @param entityInformation must not be {@literal null}. * @param operations must not be {@literal null}. */ public QuerydslKeyValueRepository(EntityInformation entityInformation, KeyValueOperations operations) { this(entityInformation, operations, SimpleEntityPathResolver.INSTANCE); } /** * Creates a new {@link QuerydslKeyValueRepository} for the given {@link EntityInformation}, * {@link KeyValueOperations} and {@link EntityPathResolver}. * * @param entityInformation must not be {@literal null}. * @param operations must not be {@literal null}. * @param resolver must not be {@literal null}. */ public QuerydslKeyValueRepository(EntityInformation entityInformation, KeyValueOperations operations, EntityPathResolver resolver) { super(entityInformation, operations); Assert.notNull(resolver, "EntityPathResolver must not be null"); this.executor = new QuerydslKeyValuePredicateExecutor<>(entityInformation, new SpelAwareProxyProjectionFactory(), operations, resolver); } @Override public Optional findOne(Predicate predicate) { return executor.findOne(predicate); } @Override public Iterable findAll(Predicate predicate) { return executor.findAll(predicate); } @Override public Iterable findAll(Predicate predicate, OrderSpecifier... orders) { return executor.findAll(predicate, orders); } @Override public Iterable findAll(Predicate predicate, Sort sort) { return executor.findAll(predicate, sort); } @Override public Page findAll(Predicate predicate, Pageable pageable) { return executor.findAll(predicate, pageable); } @Override public Iterable findAll(OrderSpecifier... orders) { return executor.findAll(orders); } @Override public long count(Predicate predicate) { return executor.count(predicate); } @Override public boolean exists(Predicate predicate) { return executor.exists(predicate); } @Override public R findBy(Predicate predicate, Function, R> queryFunction) { return executor.findBy(predicate, queryFunction); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/SimpleKeyValueRepository.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.IterableConverter; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.util.Assert; /** * Simple {@link KeyValueRepository} implementation. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch * @author Eugene Nikiforov * @param * @param */ public class SimpleKeyValueRepository implements KeyValueRepository { private final KeyValueOperations operations; private final EntityInformation entityInformation; /** * Creates a new {@link SimpleKeyValueRepository} for the given {@link EntityInformation} and * {@link KeyValueOperations}. * * @param metadata must not be {@literal null}. * @param operations must not be {@literal null}. */ public SimpleKeyValueRepository(EntityInformation metadata, KeyValueOperations operations) { Assert.notNull(metadata, "EntityInformation must not be null"); Assert.notNull(operations, "KeyValueOperations must not be null"); this.entityInformation = metadata; this.operations = operations; } // ------------------------------------------------------------------------- // Methods from CrudRepository // ------------------------------------------------------------------------- @Override public S save(S entity) { Assert.notNull(entity, "Entity must not be null"); if (entityInformation.isNew(entity)) { return operations.insert(entity); } return operations.update(entityInformation.getRequiredId(entity), entity); } @Override public List saveAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null"); List saved = new ArrayList<>(); for (S entity : entities) { saved.add(save(entity)); } return saved; } @Override public Optional findById(ID id) { Assert.notNull(id, "The given id must not be null"); return operations.findById(id, entityInformation.getJavaType()); } @Override public boolean existsById(ID id) { return findById(id).isPresent(); } @Override public List findAll() { return IterableConverter.toList(operations.findAll(entityInformation.getJavaType())); } @Override public List findAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of id's must not be null"); List result = new ArrayList<>(); ids.forEach(id -> findById(id).ifPresent(result::add)); return result; } @Override public long count() { return operations.count(entityInformation.getJavaType()); } @Override public void deleteById(ID id) { Assert.notNull(id, "The given id must not be null"); operations.delete(id, entityInformation.getJavaType()); } @Override public void delete(T entity) { Assert.notNull(entity, "The given entity must not be null"); operations.delete(entity); } @Override public void deleteAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of Ids must not be null"); ids.forEach(this::deleteById); } @Override public void deleteAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null"); entities.forEach(this::delete); } @Override public void deleteAll() { operations.delete(entityInformation.getJavaType()); } // ------------------------------------------------------------------------- // Methods from PagingAndSortingRepository // ------------------------------------------------------------------------- @Override public List findAll(Sort sort) { Assert.notNull(sort, "Sort must not be null"); return IterableConverter.toList(operations.findAll(sort, entityInformation.getJavaType())); } @Override public Page findAll(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be null"); if (pageable.isUnpaged()) { List result = findAll(); return new PageImpl<>(result, Pageable.unpaged(), result.size()); } Iterable content = operations.findInRange(pageable.getOffset(), pageable.getPageSize(), pageable.getSort(), entityInformation.getJavaType()); return new PageImpl<>(IterableConverter.toList(content), pageable, this.operations.count(entityInformation.getJavaType())); } } ================================================ FILE: src/main/java/org/springframework/data/keyvalue/repository/support/package-info.java ================================================ /** * Support infrastructure for query derivation of key/value specific repositories. */ @org.jspecify.annotations.NullMarked package org.springframework.data.keyvalue.repository.support; ================================================ FILE: src/main/java/org/springframework/data/map/KeySpaceStore.java ================================================ /* * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Strategy interface to obtain a map for a given key space. Implementations should be thread-safe when intended for use * with multiple threads (both, the store itself and the used keystore maps). *

* Can be used to plug in keystore creation or implementation strategies (for example, Map-based implementations such as * MapDB or Infinispan) through a consolidated interface. A keyspace store represents a map of maps or a database with * multiple collections and can use any kind of map per keyspace. *

* For example, a {@link ConcurrentHashMap} can be used as keystore map type to allow concurrent access to keyspaces * using: * *

 * KeyspaceStore store = KeyspaceStore.create();
 * 
* * Custom map types (or instances of these) can be used as well using the provided factory methods: * *
 * KeyspaceStore store = KeyspaceStore.of(LinkedHashMap.class);
 *
 * Map> backingMap = …;
 * KeyspaceStore store = KeyspaceStore.of(backingMap);
 * 
* * @since 4.0 */ public interface KeySpaceStore { /** * Return the map associated with given keyspace. Implementations can return an empty map if the keyspace does not * exist yet or a reference to the map that represents an existing keyspace holding keys and values for the requested * keyspace. * * @param keyspace name of the keyspace to obtain the map for, must not be {@literal null}. * @return the map associated with the given keyspace, never {@literal null}. */ Map getKeySpace(String keyspace); /** * Clear all keyspaces. Access to {@link #getKeySpace(String)} will return an empty map for each keyspace after this * method call. It is not required to clear each keyspace individually but it makes sense to do so to free up memory. */ void clear(); /** * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. * * @return a new and empty {@link KeySpaceStore}. */ static KeySpaceStore create() { return MapKeySpaceStore.create(); } /** * Create new {@link KeySpaceStore} using given map type for each keyspace map. * * @param mapType map type to use. * @return the new {@link KeySpaceStore} object. */ @SuppressWarnings("rawtypes") static KeySpaceStore of(Class mapType) { return MapKeySpaceStore.of(mapType); } /** * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. * * @param store map of maps. * @return the new {@link KeySpaceStore} object for the given {@code store}. */ static KeySpaceStore of(Map> store) { return MapKeySpaceStore.of(store); } } ================================================ FILE: src/main/java/org/springframework/data/map/MapKeySpaceStore.java ================================================ /* * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.CollectionFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * Keyspace store that uses a map of maps to store keyspace data. The outer map holds the keyspaces and the inner maps * hold the actual keys and values. * * @param store reference to the map of maps holding the keyspace data. * @param keySpaceMapType map type to be used for each keyspace map. * @param initialCapacity initial keyspace map capacity to optimize allocations. * @since 4.0 */ @SuppressWarnings("rawtypes") record MapKeySpaceStore(Map> store, Class keySpaceMapType, int initialCapacity) implements KeySpaceStore { public static final int DEFAULT_INITIAL_CAPACITY = 1000; /** * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. * * @return a new and empty {@link KeySpaceStore}. */ public static KeySpaceStore create() { return new MapKeySpaceStore(new ConcurrentHashMap<>(100), ConcurrentHashMap.class, DEFAULT_INITIAL_CAPACITY); } /** * Create new {@link KeySpaceStore} using given map type for each keyspace map. * * @param mapType map type to use. * @return the new {@link KeySpaceStore} object. */ public static KeySpaceStore of(Class mapType) { Assert.notNull(mapType, "Store map type must not be null"); return of(CollectionFactory.createMap(mapType, 100)); } /** * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. * * @param store map of maps. * @return the new {@link KeySpaceStore} object for the given {@code store}. */ @SuppressWarnings("unchecked") public static KeySpaceStore of(Map> store) { Assert.notNull(store, "Store map must not be null"); Class> userClass = (Class>) ClassUtils.getUserClass(store); return new MapKeySpaceStore(store, userClass, DEFAULT_INITIAL_CAPACITY); } @Override public Map getKeySpace(String keyspace) { return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, initialCapacity)); } @Override public void clear() { store.values().forEach(Map::clear); store.clear(); } } ================================================ FILE: src/main/java/org/springframework/data/map/MapKeyValueAdapter.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import java.util.Collection; import java.util.Comparator; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import org.jspecify.annotations.Nullable; import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.ForwardingCloseableIterator; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.PredicateQueryEngine; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; /** * {@link KeyValueAdapter} implementation for {@link Map}. * * @author Christoph Strobl * @author Derek Cochran * @author Marcel Overdijk */ public class MapKeyValueAdapter extends AbstractKeyValueAdapter { private final KeySpaceStore store; /** * Create new {@link MapKeyValueAdapter} using {@link ConcurrentHashMap} as backing store type. */ public MapKeyValueAdapter() { this(MapKeySpaceStore.create()); } /** * Create new {@link MapKeyValueAdapter} using the given query engine. * * @param engine the query engine. * @since 2.4 */ public MapKeyValueAdapter(QueryEngine engine) { this(MapKeySpaceStore.create(), engine); } /** * Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store. * * @param mapType must not be {@literal null}. */ @SuppressWarnings("rawtypes") public MapKeyValueAdapter(Class mapType) { this(MapKeySpaceStore.of(mapType)); } /** * Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store. * * @param mapType must not be {@literal null}. * @param sortAccessor accessor granting access to sorting implementation * @since 3.1.10 */ @SuppressWarnings("rawtypes") public MapKeyValueAdapter(Class mapType, SortAccessor> sortAccessor) { this(MapKeySpaceStore.of(mapType), new PredicateQueryEngine(sortAccessor)); } /** * Creates a new {@link MapKeyValueAdapter} using the given {@link Map} as backing store and query engine. * * @param mapType must not be {@literal null}. * @param engine the query engine. * @since 2.4 */ @SuppressWarnings("rawtypes") public MapKeyValueAdapter(Class mapType, QueryEngine engine) { this(MapKeySpaceStore.of(mapType), engine); } /** * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence. * * @param store must not be {@literal null}. */ public MapKeyValueAdapter(Map> store) { this(MapKeySpaceStore.of(store)); } /** * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence and query engine. * * @param store must not be {@literal null}. * @param engine the query engine. * @since 2.4 */ public MapKeyValueAdapter(Map> store, QueryEngine engine) { this(MapKeySpaceStore.of(store), engine); } /** * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence. * * @param store must not be {@literal null}. */ public MapKeyValueAdapter(KeySpaceStore store) { this(store, new PredicateQueryEngine()); } /** * Creates a new {@link MapKeyValueAdapter} with the given store and type to be used when creating key spaces and * query engine. * * @param store must not be {@literal null}. * @param engine the query engine. */ public MapKeyValueAdapter(KeySpaceStore store, @Nullable QueryEngine engine) { super(engine); Assert.notNull(store, "KeyspaceStore must not be null"); this.store = store; } @Override public @Nullable Object put(Object id, Object item, String keyspace) { Assert.notNull(id, "Cannot add item with null id"); Assert.notNull(keyspace, "Cannot add item for null collection"); return getKeySpaceMap(keyspace).put(id, item); } @Override public boolean contains(Object id, String keyspace) { return get(id, keyspace) != null; } @Override public long count(String keyspace) { return getKeySpaceMap(keyspace).size(); } @Override public @Nullable Object get(Object id, String keyspace) { Assert.notNull(id, "Cannot get item with null id"); return getKeySpaceMap(keyspace).get(id); } @Override public @Nullable Object delete(Object id, String keyspace) { Assert.notNull(id, "Cannot delete item with null id"); return getKeySpaceMap(keyspace).remove(id); } @Override public Collection getAllOf(String keyspace) { return getKeySpaceMap(keyspace).values(); } @Override public CloseableIterator> entries(String keyspace) { return new ForwardingCloseableIterator<>(getKeySpaceMap(keyspace).entrySet().iterator()); } @Override public void deleteAllOf(String keyspace) { getKeySpaceMap(keyspace).clear(); } @Override public void clear() { store.clear(); } @Override public void destroy() throws Exception { clear(); } /** * Get map associated with given key space. * * @param keyspace must not be {@literal null}. * @return */ protected Map getKeySpaceMap(String keyspace) { Assert.notNull(keyspace, "Collection must not be null for lookup"); return store.getKeySpace(keyspace); } } ================================================ FILE: src/main/java/org/springframework/data/map/package-info.java ================================================ /** * Repository implementation backed by generic {@link java.util.Map} instances. */ @org.jspecify.annotations.NullMarked package org.springframework.data.map; ================================================ FILE: src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.repository.config.QueryCreatorType; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.parser.AbstractQueryCreator; /** * Annotation to activate Map repositories. If no base package is configured through either {@link #value()}, * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(MapRepositoriesRegistrar.class) @QueryCreatorType(PredicateQueryCreator.class) public @interface EnableMapRepositories { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: * {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}. */ String[] value() default {}; /** * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. */ String[] basePackages() default {}; /** * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in * each package that serves no purpose other than being referenced by this attribute. */ Class[] basePackageClasses() default {}; /** * Specifies which types are not eligible for component scanning. */ Filter[] excludeFilters() default {}; /** * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. */ Filter[] includeFilters() default {}; /** * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning * for {@code PersonRepositoryImpl}. * * @return */ String repositoryImplementationPostfix() default "Impl"; /** * Configures the location of where to find the Spring Data named queries properties file. * * @return */ String namedQueriesLocation() default ""; /** * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to * {@link Key#CREATE_IF_NOT_FOUND}. * * @return */ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; /** * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to * {@link KeyValueRepositoryFactoryBean}. * * @return */ Class repositoryFactoryBeanClass() default KeyValueRepositoryFactoryBean.class; /** * Configure the repository base class to be used to create repository proxies for this particular configuration. * * @return */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** * Configure a specific {@link BeanNameGenerator} to be used when creating the repository beans. * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate context default. * @since 3.4 */ Class nameGenerator() default BeanNameGenerator.class; /** * Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected. * * @return */ String keyValueTemplateRef() default "mapKeyValueTemplate"; /** * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the * repositories infrastructure. */ boolean considerNestedRepositories() default false; /** * Configures the {@link Map} structure used for data storage. Defaults to {@link ConcurrentHashMap}. Will be ignored * in case an explicit bean for the {@link KeyValueTemplate} is available in the {@link ApplicationContext} or * {@link #keySpaceStoreRef()} is configured. * * @see #keyValueTemplateRef() */ @SuppressWarnings("rawtypes") Class mapType() default ConcurrentHashMap.class; /** * Configures the name to a {@link org.springframework.data.map.KeySpaceStore} bean to be used as database for all * keyspaces. * * @since 4.0 * @see org.springframework.data.map.KeySpaceStore */ String keySpaceStoreRef() default ""; /** * Configures the {@link QueryEngineFactory} to create the QueryEngine. When both, the query engine and sort accessors * are configured, the query engine is instantiated using the configured sort accessor. * * @return {@link QueryEngineFactory} to configure the QueryEngine. * @since 3.3.1 */ Class queryEngineFactory() default QueryEngineFactory.class; /** * Configures the {@code QueryCreator} to create Part-Tree queries. The QueryCreator must create queries supported by * the underlying {@code QueryEngine}. * * @return {@link AbstractQueryCreator} * @since 3.3.1 */ @AliasFor(annotation = QueryCreatorType.class, value = "value") Class> queryCreator() default PredicateQueryCreator.class; /** * Configures the {@link SortAccessor accessor} for sorting results. * * @return the configured {@link SortAccessor}. * @since 3.1.10 */ Class sortAccessor() default SortAccessor.class; } ================================================ FILE: src/main/java/org/springframework/data/map/repository/config/MapRepositoriesRegistrar.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import java.lang.annotation.Annotation; import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; import org.springframework.data.repository.config.RepositoryConfigurationExtension; /** * Map specific {@link RepositoryBeanDefinitionRegistrarSupport} implementation. * * @author Christoph Strobl */ public class MapRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { @Override protected Class getAnnotation() { return EnableMapRepositories.class; } @Override protected RepositoryConfigurationExtension getExtension() { return new MapRepositoryConfigurationExtension(); } } ================================================ FILE: src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import java.lang.reflect.Constructor; import java.util.Optional; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.data.config.ParsingUtils; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; import org.springframework.data.map.KeySpaceStore; import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.util.ClassUtils; /** * {@link RepositoryConfigurationExtension} for Map-based repositories. * * @author Christoph Strobl * @author Mark Paluch */ @SuppressWarnings("unchecked") public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension { @Override public String getModuleName() { return "Map"; } @Override protected String getModulePrefix() { return "map"; } @Override protected String getDefaultKeyValueTemplateRef() { return "mapKeyValueTemplate"; } @Override protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( RepositoryConfigurationSource configurationSource) { BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class); adapterBuilder.addConstructorArgValue(getKeySpaceStore(configurationSource)); SortAccessor sortAccessor = getSortAccessor(configurationSource); QueryEngine queryEngine = getQueryEngine(sortAccessor, configurationSource); if (queryEngine != null) { adapterBuilder.addConstructorArgValue(queryEngine); } else if (sortAccessor != null) { adapterBuilder.addConstructorArgValue(sortAccessor); } BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class); builder .addConstructorArgValue(ParsingUtils.getSourceBeanDefinition(adapterBuilder, configurationSource.getSource())); builder.setRole(BeanDefinition.ROLE_SUPPORT); return ParsingUtils.getSourceBeanDefinition(builder, configurationSource.getSource()); } private static Object getKeySpaceStore(RepositoryConfigurationSource source) { Optional keySpaceStoreRef = source.getAttribute("keySpaceStoreRef", String.class); return keySpaceStoreRef.map(beanName -> new RuntimeBeanReference(beanName, KeySpaceStore.class)) // .map(Object.class::cast) // .orElseGet(() -> source.getRequiredAttribute("mapType", Class.class)); } private static @Nullable SortAccessor getSortAccessor(RepositoryConfigurationSource source) { Class> sortAccessorType = getClassAttribute(source, "sortAccessor"); if (sortAccessorType == null) { return null; } return BeanUtils.instantiateClass(sortAccessorType); } private static @Nullable QueryEngine getQueryEngine(@Nullable SortAccessor sortAccessor, RepositoryConfigurationSource source) { Class queryEngineFactoryType = getClassAttribute(source, "queryEngineFactory"); if (queryEngineFactoryType == null) { return null; } if (sortAccessor != null) { Constructor constructor = ClassUtils .getConstructorIfAvailable(queryEngineFactoryType, SortAccessor.class); if (constructor != null) { return BeanUtils.instantiateClass(constructor, sortAccessor).create(); } } return BeanUtils.instantiateClass(queryEngineFactoryType).create(); } private static @Nullable Class getClassAttribute(RepositoryConfigurationSource source, String attributeName) { return source.getAttribute(attributeName, Class.class).filter(Predicate.not(Class::isInterface)).orElse(null); } } ================================================ FILE: src/main/java/org/springframework/data/map/repository/config/package-info.java ================================================ /** * Support infrastructure for the configuration of {@link java.util.Map} repositories. */ @org.jspecify.annotations.NullMarked package org.springframework.data.map.repository.config; ================================================ FILE: src/main/resources/META-INF/spring/aot.factories ================================================ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.data.keyvalue.aot.KeyValueRuntimeHints ================================================ FILE: src/main/resources/META-INF/spring.factories ================================================ org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory ================================================ FILE: src/main/resources/license.txt ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ======================================================================= SPRING FRAMEWORK ${version} SUBCOMPONENTS: Spring Framework ${version} includes a number of subcomponents with separate copyright notices and license terms. The product that includes this file does not necessarily use all the open source subcomponents referred to below. Your use of the source code for these subcomponents is subject to the terms and conditions of the following licenses. >>> ASM 4.0 (org.ow2.asm:asm:4.0, org.ow2.asm:asm-commons:4.0): Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (c) 1999-2009, OW2 Consortium >>> CGLIB 3.0 (cglib:cglib:3.0): Per the LICENSE file in the CGLIB JAR distribution downloaded from https://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download, CGLIB 3.0 is licensed under the Apache License, version 2.0, the text of which is included above. ======================================================================= To the extent any open source subcomponents are licensed under the EPL and/or other similar licenses that require the source code and/or modifications to source code to be made available (as would be noted above), you may obtain a copy of the source code corresponding to the binaries for such open source components and modifications thereto, if any, (the "Source Files"), by downloading the Source Files from https://www.springsource.org/download, or by sending a request, with your name and address to: VMware, Inc., 3401 Hillview Avenue Palo Alto, CA 94304 United States of America or email info@vmware.com. All such requests should clearly specify: OPEN SOURCE FILES REQUEST Attention General Counsel VMware shall mail a copy of the Source Files to you on a CD or equivalent physical medium. This offer to obtain a copy of the Source Files is valid for three years from the date you acquired this Software product. ================================================ FILE: src/main/resources/notice.txt ================================================ Spring Data KeyValue 4.1 RC1 (2026.0.0) Copyright (c) 2015-2019 Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). You may not use this product except in compliance with the License. This product may include a number of subcomponents with separate copyright notices and license terms. Your use of the source code for these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the license.txt file. ================================================ FILE: src/test/java/org/springframework/data/keyvalue/CustomKeySpaceAnnotationWithAliasFor.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.annotation.KeySpace; /** * Custom composed {@link Persistent} annotation using {@link AliasFor} on name attribute. * * @author Christoph Strobl */ @Persistent @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) @KeySpace public @interface CustomKeySpaceAnnotationWithAliasFor { @AliasFor(annotation = KeySpace.class, attribute = "value") String name() default ""; } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/Person.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import java.net.URL; import org.springframework.data.annotation.Id; import com.querydsl.core.annotations.QueryEntity; /** * @author Christoph Strobl * @author Mark Paluch */ @QueryEntity public class Person { private @Id String id; private String firstname; private int age; private URL homepage; public Person(String firstname, int age) { super(); this.firstname = firstname; this.age = age; } public String getId() { return this.id; } public String getFirstname() { return this.firstname; } public int getAge() { return this.age; } public void setId(String id) { this.id = id; } public void setFirstname(String firstname) { this.firstname = firstname; } public void setAge(int age) { this.age = age; } public URL getHomepage() { return homepage; } public void setHomepage(URL homepage) { this.homepage = homepage; } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/SubclassOfTypeWithCustomComposedKeySpaceAnnotation.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import org.springframework.data.keyvalue.annotation.KeySpace; /** * Class that inherits its {@link KeySpace} from a super class annotated with a custom {@link CustomKeySpaceAnnotation} * annotation. * * @author Christoph Strobl */ public class SubclassOfTypeWithCustomComposedKeySpaceAnnotation extends TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor { public SubclassOfTypeWithCustomComposedKeySpaceAnnotation(String name) { super(name); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Persistent; /** * A {@link Persistent} type with {@link CustomKeySpaceAnnotationWithAliasFor}. * * @author Christoph Strobl * @author Mark Paluch */ @CustomKeySpaceAnnotationWithAliasFor(name = "aliased") public class TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor { @Id String id; String name; public TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor(String name) { this.name = name; } public String getId() { return this.id; } public String getName() { return this.name; } public void setId(String id) { this.id = id; } public void setName(String name) { this.name = name; } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/TypeWithDirectKeySpaceAnnotation.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.annotation.KeySpace; /** * A {@link Persistent} type with explict {@link KeySpace}. * * @author Christoph Strobl */ @KeySpace("rhaegar") public class TypeWithDirectKeySpaceAnnotation { } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/TypeWithInhteritedPersistentAnnotationNotHavingKeySpace.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import org.springframework.data.annotation.Persistent; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.keyvalue.annotation.KeySpace; /** * A type inheriting {@link Persistent} from {@link TypeAlias} not having a {@link KeySpace} defined. * * @author Christoph Strobl */ @TypeAlias("foo") public class TypeWithInhteritedPersistentAnnotationNotHavingKeySpace { } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/TypeWithPersistentAnnotationNotHavingKeySpace.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.annotation.KeySpace; /** * A {@link Persistent} class without a defined {@link KeySpace}. * * @author Christoph Strobl */ @Persistent public class TypeWithPersistentAnnotationNotHavingKeySpace { } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/DefaultIdentifierGeneratorUnitTests.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import java.util.Date; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.core.TypeInformation; /** * @author Christoph Strobl */ class DefaultIdentifierGeneratorUnitTests { private DefaultIdentifierGenerator generator = DefaultIdentifierGenerator.INSTANCE; @Test void shouldThrowExceptionForUnsupportedType() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) .isThrownBy(() -> generator.generateIdentifierOfType(TypeInformation.of(Date.class))); } @Test // DATAKV-136 void shouldGenerateUUIDValueCorrectly() { Object value = generator.generateIdentifierOfType(TypeInformation.of(UUID.class)); assertThat(value).isNotNull().isInstanceOf(UUID.class); } @Test // DATAKV-136 void shouldGenerateStringValueCorrectly() { Object value = generator.generateIdentifierOfType(TypeInformation.of(String.class)); assertThat(value).isNotNull().isInstanceOf(String.class); } @Test // DATAKV-136 void shouldGenerateLongValueCorrectly() { Object value = generator.generateIdentifierOfType(TypeInformation.of(Long.class)); assertThat(value).isNotNull().isInstanceOf(Long.class); } @Test // DATAKV-136 void shouldGenerateIntValueCorrectly() { Object value = generator.generateIdentifierOfType(TypeInformation.of(Integer.class)); assertThat(value).isNotNull().isInstanceOf(Integer.class); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/ForwardingCloseableIteratorUnitTests.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.util.CloseableIterator; /** * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke */ @ExtendWith(MockitoExtension.class) class ForwardingCloseableIteratorUnitTests { @Mock Iterator> iteratorMock; @Mock Runnable closeActionMock; @Test // DATAKV-99 void hasNextShouldDelegateToWrappedIterator() { when(iteratorMock.hasNext()).thenReturn(true); CloseableIterator> iterator = new ForwardingCloseableIterator<>(iteratorMock); try { assertThat(iterator.hasNext()).isTrue(); verify(iteratorMock, times(1)).hasNext(); } finally { iterator.close(); } } @Test // DATAKV-99 @SuppressWarnings("unchecked") void nextShouldDelegateToWrappedIterator() { when(iteratorMock.next()).thenReturn((Entry) mock(Map.Entry.class)); CloseableIterator> iterator = new ForwardingCloseableIterator<>(iteratorMock); try { assertThat(iterator.next()).isNotNull(); verify(iteratorMock, times(1)).next(); } finally { iterator.close(); } } @Test // DATAKV-99 void nextShouldThrowErrorWhenWrappedIteratorHasNoMoreElements() { when(iteratorMock.next()).thenThrow(new NoSuchElementException()); CloseableIterator> iterator = new ForwardingCloseableIterator<>(iteratorMock); try { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(iterator::next); } finally { iterator.close(); } } @Test // DATAKV-99 void closeShouldDoNothingByDefault() { new ForwardingCloseableIterator<>(iteratorMock).close(); verifyNoInteractions(iteratorMock); } @Test // DATAKV-99 void closeShouldInvokeConfiguredCloseAction() { new ForwardingCloseableIterator<>(iteratorMock, closeActionMock).close(); verify(closeActionMock, times(1)).run(); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/IterableConverterUnitTests.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.keyvalue.core.IterableConverter.*; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; /** * @author Christoph Strobl * @author Mark Paluch */ class IterableConverterUnitTests { @Test // DATAKV-101 void toListShouldReturnEmptyListWhenSourceEmpty() { assertThat(toList(Collections.emptySet())).isEmpty(); } @Test // DATAKV-101 void toListShouldReturnSameObjectWhenSourceIsAlreadyListType() { List source = new ArrayList<>(); assertThat(toList(source)).isSameAs(source); } @Test // DATAKV-101 void toListShouldReturnListWhenSourceIsNonListType() { Set source = new HashSet<>(); source.add("tyrion"); assertThat(toList(source)).isInstanceOf(List.class); } @Test // DATAKV-101 void toListShouldHoldValuesInOrderOfSource() { Set source = new LinkedHashSet<>(); source.add("tyrion"); source.add("jaime"); assertThat(toList(source)).containsExactly(source.toArray(new String[2])); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/KeyValuePersistenceExceptionTranslatorUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.dao.DataRetrievalFailureException; /** * @author Christoph Strobl */ class KeyValuePersistenceExceptionTranslatorUnitTests { private KeyValuePersistenceExceptionTranslator translator = new KeyValuePersistenceExceptionTranslator(); @Test // DATACMNS-525 void translateExeptionShouldReturnDataAccessExceptionWhenGivenOne() { assertThat(translator.translateExceptionIfPossible(new DataRetrievalFailureException("booh"))) .isInstanceOf(DataRetrievalFailureException.class); } @Test // DATACMNS-525, DATAKV-192 void translateExeptionShouldReturnNullWhenGivenNull() { assertThatIllegalArgumentException() .isThrownBy(() -> assertThat(translator.translateExceptionIfPossible(null)).isNull()); } @Test // DATACMNS-525 void translateExeptionShouldTranslateNoSuchElementExceptionToDataRetrievalFailureException() { assertThat(translator.translateExceptionIfPossible(new NoSuchElementException(""))) .isInstanceOf(DataRetrievalFailureException.class); } @Test // DATACMNS-525 void translateExeptionShouldTranslateIndexOutOfBoundsExceptionToDataRetrievalFailureException() { assertThat(translator.translateExceptionIfPossible(new IndexOutOfBoundsException(""))) .isInstanceOf(DataRetrievalFailureException.class); } @Test // DATACMNS-525 void translateExeptionShouldTranslateIllegalStateExceptionToDataRetrievalFailureException() { assertThat(translator.translateExceptionIfPossible(new IllegalStateException(""))) .isInstanceOf(DataRetrievalFailureException.class); } @Test // DATACMNS-525 void translateExeptionShouldTranslateAnyJavaExceptionToUncategorizedKeyValueException() { assertThat(translator.translateExceptionIfPossible(new UnsupportedOperationException(""))) .isInstanceOf(UncategorizedKeyValueException.class); } @Test // DATACMNS-525 void translateExeptionShouldReturnNullForNonJavaExceptions() { assertThat(translator.translateExceptionIfPossible(new NoSuchBeanDefinitionException(""))).isNull(); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.annotation.KeySpace; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.map.MapKeyValueAdapter; /** * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ class KeyValueTemplateTests { private static final Foo FOO_ONE = new Foo("one"); private static final Foo FOO_TWO = new Foo("two"); private static final Foo FOO_THREE = new Foo("three"); private static final Bar BAR_ONE = new Bar("one"); private static final ClassWithTypeAlias ALIASED = new ClassWithTypeAlias("super"); private static final SubclassOfAliasedType SUBCLASS_OF_ALIASED = new SubclassOfAliasedType("sub"); private static final KeyValueQuery> STRING_QUERY = new KeyValueQuery<>((Predicate) foo -> foo.getFoo().equals("two")); private KeyValueTemplate operations; @BeforeEach void setUp() { this.operations = new KeyValueTemplate(new MapKeyValueAdapter()); } @AfterEach void tearDown() throws Exception { this.operations.destroy(); } @Test // DATACMNS-525 void insertShouldNotThorwErrorWhenExecutedHavingNonExistingIdAndNonNullValue() { operations.insert("1", FOO_ONE); } @Test // DATACMNS-525 void insertShouldThrowExceptionForNullId() { assertThatIllegalArgumentException().isThrownBy(() -> operations.insert(null, FOO_ONE)); } @Test // DATACMNS-525 void insertShouldThrowExceptionForNullObject() { assertThatIllegalArgumentException().isThrownBy(() -> operations.insert("some-id", null)); } @Test // DATACMNS-525 void insertShouldThrowExecptionWhenObjectOfSameTypeAlreadyExists() { operations.insert("1", FOO_ONE); assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(() -> operations.insert("1", FOO_TWO)); } @Test // DATACMNS-525 void insertShouldWorkCorrectlyWhenObjectsOfDifferentTypesWithSameIdAreInserted() { operations.insert("1", FOO_ONE); operations.insert("1", BAR_ONE); } @Test // DATACMNS-525 void createShouldReturnSameInstanceGenerateId() { ClassWithStringId source = new ClassWithStringId(); ClassWithStringId target = operations.insert(source); assertThat(target).isSameAs(source); } @Test // DATACMNS-525 void createShouldRespectExistingId() { ClassWithStringId source = new ClassWithStringId(); source.id = "one"; operations.insert(source); assertThat(operations.findById("one", ClassWithStringId.class)).contains(source); } @Test // DATACMNS-525 void findByIdShouldReturnObjectWithMatchingIdAndType() { operations.insert("1", FOO_ONE); assertThat(operations.findById("1", Foo.class)).contains(FOO_ONE); } @Test // DATACMNS-525 void findByIdSouldReturnOptionalEmptyIfNoMatchingIdFound() { operations.insert("1", FOO_ONE); assertThat(operations.findById("2", Foo.class)).isEmpty(); } @Test // DATACMNS-525 void findByIdShouldReturnOptionalEmptyIfNoMatchingTypeFound() { operations.insert("1", FOO_ONE); assertThat(operations.findById("1", Bar.class)).isEmpty(); } @Test // DATACMNS-525 void findShouldExecuteQueryCorrectly() { operations.insert("1", FOO_ONE); operations.insert("2", FOO_TWO); List result = (List) operations.find(STRING_QUERY, Foo.class); assertThat(result).hasSize(1); assertThat(result.get(0)).isEqualTo(FOO_TWO); } @Test // DATACMNS-525 void readShouldReturnEmptyCollectionIfOffsetOutOfRange() { operations.insert("1", FOO_ONE); operations.insert("2", FOO_TWO); operations.insert("3", FOO_THREE); assertThat(operations.findInRange(5, 5, Foo.class)).isEmpty(); } @Test // DATACMNS-525 void updateShouldReplaceExistingObject() { operations.insert("1", FOO_ONE); operations.update("1", FOO_TWO); assertThat(operations.findById("1", Foo.class)).contains(FOO_TWO); } @Test // DATACMNS-525 void updateShouldRespectTypeInformation() { operations.insert("1", FOO_ONE); operations.update("1", BAR_ONE); assertThat(operations.findById("1", Foo.class)).contains(FOO_ONE); } @Test // DATACMNS-525 void deleteShouldRemoveObjectCorrectly() { operations.insert("1", FOO_ONE); operations.delete("1", Foo.class); assertThat(operations.findById("1", Foo.class)).isEmpty(); } @Test // DATACMNS-525 void deleteReturnsNullWhenNotExisting() { operations.insert("1", FOO_ONE); assertThat(operations.delete("2", Foo.class)).isNull(); } @Test // DATACMNS-525 void deleteReturnsRemovedObject() { operations.insert("1", FOO_ONE); assertThat(operations.delete("1", Foo.class)).isEqualTo(FOO_ONE); } @Test // DATACMNS-525 void deleteThrowsExceptionWhenIdCannotBeExctracted() { assertThatIllegalStateException().isThrownBy(() -> operations.delete(FOO_ONE)); } @Test // DATACMNS-525 void countShouldReturnZeroWhenNoElementsPresent() { assertThat(operations.count(Foo.class)).isEqualTo(0L); } @Test // DATACMNS-525 void insertShouldRespectTypeAlias() { operations.insert("1", ALIASED); operations.insert("2", SUBCLASS_OF_ALIASED); assertThat((List) operations.findAll(ALIASED.getClass())).contains(ALIASED, SUBCLASS_OF_ALIASED); } static class Foo { String foo; public Foo(String foo) { this.foo = foo; } public String getFoo() { return this.foo; } public void setFoo(String foo) { this.foo = foo; } } static class Bar { String bar; public Bar(String bar) { this.bar = bar; } public String getBar() { return this.bar; } public void setBar(String bar) { this.bar = bar; } } static class ClassWithStringId implements Serializable { private static final long serialVersionUID = -7481030649267602830L; @Id String id; String value; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } @ExplicitKeySpace(name = "aliased") static class ClassWithTypeAlias implements Serializable { private static final long serialVersionUID = -5921943364908784571L; @Id String id; String name; ClassWithTypeAlias(String name) { this.name = name; } public String getId() { return this.id; } public String getName() { return this.name; } public void setId(String id) { this.id = id; } public void setName(String name) { this.name = name; } } static class SubclassOfAliasedType extends ClassWithTypeAlias { private static final long serialVersionUID = -468809596668871479L; SubclassOfAliasedType(String name) { super(name); } } @KeySpace @Persistent @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) @interface ExplicitKeySpace { @AliasFor(annotation = KeySpace.class, value = "value") String name() default ""; } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.SubclassOfTypeWithCustomComposedKeySpaceAnnotation; import org.springframework.data.keyvalue.TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor; import org.springframework.data.keyvalue.core.event.KeyValueEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterDeleteEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterDropKeySpaceEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterGetEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterInsertEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterUpdateEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeDeleteEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeGetEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeInsertEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeUpdateEvent; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class KeyValueTemplateUnitTests { private static final Foo FOO_ONE = new Foo("one"); private static final Foo FOO_TWO = new Foo("two"); private static final TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor ALIASED_USING_ALIAS_FOR = new TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor( "super"); private static final SubclassOfTypeWithCustomComposedKeySpaceAnnotation SUBCLASS_OF_ALIASED_USING_ALIAS_FOR = new SubclassOfTypeWithCustomComposedKeySpaceAnnotation( "sub"); private static final KeyValueQuery STRING_QUERY = new KeyValueQuery<>("foo == 'two'"); private @Mock KeyValueAdapter adapterMock; private KeyValueTemplate template; private @Mock ApplicationEventPublisher publisherMock; @BeforeEach void setUp() { this.template = new KeyValueTemplate(adapterMock); this.template.setApplicationEventPublisher(publisherMock); } @Test // DATACMNS-525 void shouldThrowExceptionWhenCreatingNewTempateWithNullAdapter() { assertThatIllegalArgumentException().isThrownBy(() -> new KeyValueTemplate(null)); } @Test // DATACMNS-525 void shouldThrowExceptionWhenCreatingNewTempateWithNullMappingContext() { assertThatIllegalArgumentException().isThrownBy(() -> new KeyValueTemplate(adapterMock, null)); } @Test // DATACMNS-525 void insertShouldLookUpValuesBeforeInserting() { template.insert("1", FOO_ONE); verify(adapterMock, times(1)).contains("1", Foo.class.getName()); } @Test // DATACMNS-525 void insertShouldInsertUseClassNameAsDefaultKeyspace() { template.insert("1", FOO_ONE); verify(adapterMock, times(1)).put("1", FOO_ONE, Foo.class.getName()); } @Test // DATACMNS-225 void insertShouldReturnInsertedObject() { ClassWithStringId object = new ClassWithStringId(); assertThat(template.insert(object)).isEqualTo(object); assertThat(template.insert("1", object)).isEqualTo(object); } @Test // DATACMNS-525 void insertShouldThrowExceptionWhenObectWithIdAlreadyExists() { when(adapterMock.contains(anyString(), anyString())).thenReturn(true); assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(() -> template.insert("1", FOO_ONE)); } @Test // DATACMNS-525 void insertShouldThrowExceptionForNullId() { assertThatIllegalArgumentException().isThrownBy(() -> template.insert(null, FOO_ONE)); } @Test // DATACMNS-525 void insertShouldThrowExceptionForNullObject() { assertThatIllegalArgumentException().isThrownBy(() -> template.insert("some-id", null)); } @Test // DATACMNS-525 void insertShouldGenerateId() { ClassWithStringId target = template.insert(new ClassWithStringId()); assertThat(target.id).isNotNull(); } @Test // DATACMNS-525 void insertShouldThrowErrorWhenIdCannotBeResolved() { assertThatIllegalStateException().isThrownBy(() -> template.insert(FOO_ONE)); } @Test // DATACMNS-525 void insertShouldReturnSameInstanceGenerateId() { ClassWithStringId source = new ClassWithStringId(); ClassWithStringId target = template.insert(source); assertThat(target).isSameAs(source); } @Test // DATACMNS-525 void insertShouldRespectExistingId() { ClassWithStringId source = new ClassWithStringId(); source.id = "one"; template.insert(source); verify(adapterMock, times(1)).put("one", source, ClassWithStringId.class.getName()); } @Test // DATACMNS-525 void findByIdShouldReturnOptionalEmptyWhenNoElementsPresent() { assertThat(template.findById("1", Foo.class)).isEmpty(); } @Test // DATACMNS-525 void findByIdShouldReturnObjectWithMatchingIdAndType() { template.findById("1", Foo.class); verify(adapterMock, times(1)).get("1", Foo.class.getName(), Foo.class); } @Test // DATACMNS-525, DATAKV-187 void findByIdShouldThrowExceptionWhenGivenNullId() { assertThatIllegalArgumentException().isThrownBy(() -> template.findById(null, Foo.class)); } @Test // DATACMNS-525 void findAllOfShouldReturnEntireCollection() { template.findAll(Foo.class); verify(adapterMock, times(1)).getAllOf(Foo.class.getName(), Foo.class); } @Test // DATACMNS-525 void findAllOfShouldThrowExceptionWhenGivenNullType() { assertThatIllegalArgumentException().isThrownBy(() -> template.findAll(null)); } @Test // DATACMNS-525 void findShouldCallFindOnAdapterToResolveMatching() { template.find(STRING_QUERY, Foo.class); verify(adapterMock, times(1)).find(STRING_QUERY, Foo.class.getName(), Foo.class); } @Test // DATACMNS-525 @SuppressWarnings("rawtypes") void findInRangeShouldRespectOffset() { ArgumentCaptor captor = ArgumentCaptor.forClass(KeyValueQuery.class); template.findInRange(1, 5, Foo.class); verify(adapterMock, times(1)).find(captor.capture(), eq(Foo.class.getName()), eq(Foo.class)); assertThat(captor.getValue().getOffset()).isEqualTo(1L); assertThat(captor.getValue().getRows()).isEqualTo(5); assertThat(captor.getValue().getCriteria()).isNull(); } @Test // DATACMNS-525 void updateShouldReplaceExistingObject() { template.update("1", FOO_TWO); verify(adapterMock, times(1)).put("1", FOO_TWO, Foo.class.getName()); } @Test // DATAKV-225 void updateShouldReturnUpdatedObject() { ClassWithStringId object = new ClassWithStringId(); object.id = "foo"; assertThat(template.update(object)).isEqualTo(object); assertThat(template.update("1", object)).isEqualTo(object); } @Test // DATACMNS-525 void updateShouldThrowExceptionWhenGivenNullId() { assertThatIllegalArgumentException().isThrownBy(() -> template.update(null, FOO_ONE)); } @Test // DATACMNS-525 void updateShouldThrowExceptionWhenGivenNullObject() { assertThatIllegalArgumentException().isThrownBy(() -> template.update("1", null)); } @Test // DATACMNS-525 void updateShouldUseExtractedIdInformation() { ClassWithStringId source = new ClassWithStringId(); source.id = "some-id"; template.update(source); verify(adapterMock, times(1)).put(source.id, source, ClassWithStringId.class.getName()); } @Test // DATACMNS-525 void updateShouldThrowErrorWhenIdInformationCannotBeExtracted() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> template.update(FOO_ONE)); } @Test // DATACMNS-525 void deleteShouldRemoveObjectCorrectly() { template.delete("1", Foo.class); verify(adapterMock, times(1)).delete("1", Foo.class.getName(), Foo.class); } @Test // DATACMNS-525 void deleteRemovesObjectUsingExtractedId() { ClassWithStringId source = new ClassWithStringId(); source.id = "some-id"; template.delete(source); verify(adapterMock, times(1)).delete("some-id", ClassWithStringId.class.getName(), ClassWithStringId.class); } @Test // DATACMNS-525 void deleteThrowsExceptionWhenIdCannotBeExctracted() { assertThatIllegalStateException().isThrownBy(() -> template.delete(FOO_ONE)); } @Test // DATACMNS-525 void countShouldReturnZeroWhenNoElementsPresent() { template.count(Foo.class); } @Test // DATACMNS-525 void countShouldReturnCollectionSize() { when(adapterMock.count(Foo.class.getName())).thenReturn(2L); assertThat(template.count(Foo.class)).isEqualTo(2L); } @Test // DATACMNS-525 void countShouldThrowErrorOnNullType() { assertThatIllegalArgumentException().isThrownBy(() -> template.count(null)); } @Test // DATACMNS-525 void insertShouldRespectTypeAlias() { template.insert("1", ALIASED_USING_ALIAS_FOR); verify(adapterMock, times(1)).put("1", ALIASED_USING_ALIAS_FOR, "aliased"); } @Test // DATACMNS-525 void insertShouldRespectTypeAliasOnSubClass() { template.insert("1", SUBCLASS_OF_ALIASED_USING_ALIAS_FOR); verify(adapterMock, times(1)).put("1", SUBCLASS_OF_ALIASED_USING_ALIAS_FOR, "aliased"); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test // DATACMNS-525 void findAllOfShouldRespectTypeAliasAndFilterNonMatchingTypes() { Collection foo = Arrays.asList(ALIASED_USING_ALIAS_FOR, SUBCLASS_OF_ALIASED_USING_ALIAS_FOR); when(adapterMock.getAllOf("aliased", SUBCLASS_OF_ALIASED_USING_ALIAS_FOR.getClass())).thenReturn(foo); assertThat((Iterable) template.findAll(SUBCLASS_OF_ALIASED_USING_ALIAS_FOR.getClass())) .contains(SUBCLASS_OF_ALIASED_USING_ALIAS_FOR); } @Test // DATACMNS-525 void insertSouldRespectTypeAliasAndFilterNonMatching() { template.insert("1", ALIASED_USING_ALIAS_FOR); assertThat(template.findById("1", SUBCLASS_OF_ALIASED_USING_ALIAS_FOR.getClass())).isEmpty(); } @Test // DATACMNS-525 void setttingNullPersistenceExceptionTranslatorShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> template.setExceptionTranslator(null)); } @Test // DATAKV-91 void shouldNotPublishEventWhenNoApplicationContextSet() { template.setApplicationEventPublisher(null); template.insert("1", FOO_ONE); verifyNoInteractions(publisherMock); } @Test // DATAKV-104 void shouldNotPublishEventsWhenEventsToPublishIsSetToNull() { template.setEventTypesToPublish(null); template.insert("1", FOO_ONE); verifyNoInteractions(publisherMock); } @Test // DATAKV-104 void shouldNotPublishEventsWhenEventsToPublishIsSetToEmptyList() { template.setEventTypesToPublish(Collections.emptySet()); template.insert("1", FOO_ONE); verifyNoInteractions(publisherMock); } @Test // DATAKV-104 void shouldPublishEventsByDefault() { template.insert("1", FOO_ONE); verify(publisherMock, atLeastOnce()).publishEvent(any()); } @Test // DATAKV-91, DATAKV-104 void shouldNotPublishEventWhenNotExplicitlySetForPublication() { setEventsToPublish(BeforeDeleteEvent.class); template.insert("1", FOO_ONE); verifyNoInteractions(publisherMock); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishBeforeInsertEventCorrectly() { setEventsToPublish(BeforeInsertEvent.class); template.insert("1", FOO_ONE); ArgumentCaptor captor = ArgumentCaptor.forClass(BeforeInsertEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishAfterInsertEventCorrectly() { setEventsToPublish(AfterInsertEvent.class); template.insert("1", FOO_ONE); ArgumentCaptor captor = ArgumentCaptor.forClass(AfterInsertEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishBeforeUpdateEventCorrectly() { setEventsToPublish(BeforeUpdateEvent.class); template.update("1", FOO_ONE); ArgumentCaptor captor = ArgumentCaptor.forClass(BeforeUpdateEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishAfterUpdateEventCorrectly() { setEventsToPublish(AfterUpdateEvent.class); template.update("1", FOO_ONE); ArgumentCaptor captor = ArgumentCaptor.forClass(AfterUpdateEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishBeforeDeleteEventCorrectly() { setEventsToPublish(BeforeDeleteEvent.class); template.delete("1", FOO_ONE.getClass()); ArgumentCaptor captor = ArgumentCaptor.forClass(BeforeDeleteEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishAfterDeleteEventCorrectly() { setEventsToPublish(AfterDeleteEvent.class); when(adapterMock.delete(eq("1"), eq(FOO_ONE.getClass().getName()), eq(Foo.class))).thenReturn(FOO_ONE); template.delete("1", FOO_ONE.getClass()); ArgumentCaptor captor = ArgumentCaptor.forClass(AfterDeleteEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishBeforeGetEventCorrectly() { setEventsToPublish(BeforeGetEvent.class); when(adapterMock.get(eq("1"), eq(FOO_ONE.getClass().getName()))).thenReturn(FOO_ONE); template.findById("1", FOO_ONE.getClass()); ArgumentCaptor captor = ArgumentCaptor.forClass(BeforeGetEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); } @Test // DATAKV-91, DATAKV-104 @SuppressWarnings({ "rawtypes" }) void shouldPublishAfterGetEventCorrectly() { setEventsToPublish(AfterGetEvent.class); when(adapterMock.get(eq("1"), eq(FOO_ONE.getClass().getName()), eq(Foo.class))).thenReturn(FOO_ONE); template.findById("1", FOO_ONE.getClass()); ArgumentCaptor captor = ArgumentCaptor.forClass(AfterGetEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKey()).isEqualTo("1"); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); assertThat(captor.getValue().getPayload()).isEqualTo(FOO_ONE); } @Test // DATAKV-91, DATAKV-104, DATAKV-187 @SuppressWarnings({ "rawtypes" }) void shouldPublishDropKeyspaceEventCorrectly() { setEventsToPublish(AfterDropKeySpaceEvent.class); template.delete(FOO_ONE.getClass()); ArgumentCaptor captor = ArgumentCaptor.forClass(AfterDropKeySpaceEvent.class); verify(publisherMock, times(1)).publishEvent(captor.capture()); verifyNoMoreInteractions(publisherMock); assertThat(captor.getValue().getKeyspace()).isEqualTo(Foo.class.getName()); } @Test // DATAKV-129 void insertShouldRespectTypeAliasUsingAliasFor() { template.insert("1", ALIASED_USING_ALIAS_FOR); verify(adapterMock, times(1)).put("1", ALIASED_USING_ALIAS_FOR, "aliased"); } @SafeVarargs @SuppressWarnings("rawtypes") private final void setEventsToPublish(Class... events) { template.setEventTypesToPublish(new HashSet<>(Arrays.asList(events))); } static class Foo { String foo; public Foo(String foo) { this.foo = foo; } public String getFoo() { return this.foo; } public void setFoo(String foo) { this.foo = foo; } } class Bar { String bar; public Bar(String bar) { this.bar = bar; } public String getBar() { return this.bar; } public void setBar(String bar) { this.bar = bar; } } static class ClassWithStringId { @Id String id; String value; public String getId() { return this.id; } public String getValue() { return this.value; } public void setId(String id) { this.id = id; } public void setValue(String value) { this.value = value; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/PredicateQueryEngineUnitTests.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.annotation.Id; import org.springframework.data.core.TypeInformation; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.PartTree; /** * Unit tests for {@link SpelQueryEngine}. * * @author Christoph Strobl */ @ExtendWith(MockitoExtension.class) public class PredicateQueryEngineUnitTests { private static final Person BOB_WITH_FIRSTNAME = new Person("bob", 30); private static final Person MIKE_WITHOUT_FIRSTNAME = new Person(null, 25); @Mock KeyValueAdapter adapter; private PredicateQueryEngine engine; private Iterable people = Arrays.asList(BOB_WITH_FIRSTNAME, MIKE_WITHOUT_FIRSTNAME); @BeforeEach void setUp() { engine = new PredicateQueryEngine(); engine.registerAdapter(adapter); } @Test // DATAKV-114 @SuppressWarnings("unchecked") void queriesEntitiesWithNullProperty() throws Exception { doReturn(people).when(adapter).getAllOf(anyString()); Collection result = engine.execute(createQueryForMethodWithArgs("findByFirstname", "bob"), null, -1, -1, anyString()); assertThat(result).containsExactly(BOB_WITH_FIRSTNAME); } @Test // DATAKV-114 void countsEntitiesWithNullProperty() throws Exception { doReturn(people).when(adapter).getAllOf(anyString()); assertThat(engine.count(createQueryForMethodWithArgs("findByFirstname", "bob"), anyString())).isEqualTo(1L); } private static Predicate createQueryForMethodWithArgs(String methodName, Object... args) throws Exception { List> types = new ArrayList<>(args.length); for (Object arg : args) { types.add(arg.getClass()); } Method method = PersonRepository.class.getMethod(methodName, types.toArray(new Class[types.size()])); RepositoryMetadata metadata = mock(RepositoryMetadata.class); doReturn(method.getReturnType()).when(metadata).getReturnedDomainClass(method); doReturn(TypeInformation.fromReturnTypeOf(method)).when(metadata).getReturnType(method); doReturn(TypeInformation.of(Person.class)).when(metadata).getDomainTypeInformation(); PartTree partTree = new PartTree(method.getName(), method.getReturnType()); PredicateQueryCreator creator = new PredicateQueryCreator(partTree, new ParametersParameterAccessor( new QueryMethod(method, metadata, new SpelAwareProxyProjectionFactory()).getParameters(), args)); return creator.createQuery().getCriteria(); } interface PersonRepository { Person findByFirstname(String firstname); } public static class Person { @Id String id; String firstname; int age; Person(String firstname, int age) { this.firstname = firstname; this.age = age; } public String getFirstname() { return firstname; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/PropertyPathComparatorUnitTests.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import java.util.Comparator; import org.junit.jupiter.api.Test; /** * Unit tests for {@link PropertyPathComparator}. * * @author Christoph Strobl */ class PropertyPathComparatorUnitTests { private static final SomeType ONE = new SomeType("one", 1, 1); private static final SomeType TWO = new SomeType("two", 2, 2); private static final WrapperType WRAPPER_ONE = new WrapperType("w-one", ONE); private static final WrapperType WRAPPER_TWO = new WrapperType("w-two", TWO); @Test // DATACMNS-525 void shouldCompareStringAscCorrectly() { Comparator comparator = new PropertyPathComparator<>("stringProperty"); assertThat(comparator.compare(ONE, TWO)).isEqualTo(ONE.getStringProperty().compareTo(TWO.getStringProperty())); } @Test // DATACMNS-525 void shouldCompareStringDescCorrectly() { Comparator comparator = new PropertyPathComparator("stringProperty").desc(); assertThat(comparator.compare(ONE, TWO)).isEqualTo(TWO.getStringProperty().compareTo(ONE.getStringProperty())); } @Test // DATACMNS-525 void shouldCompareIntegerAscCorrectly() { Comparator comparator = new PropertyPathComparator<>("integerProperty"); assertThat(comparator.compare(ONE, TWO)).isEqualTo(ONE.getIntegerProperty().compareTo(TWO.getIntegerProperty())); } @Test // DATACMNS-525 void shouldCompareIntegerDescCorrectly() { Comparator comparator = new PropertyPathComparator("integerProperty").desc(); assertThat(comparator.compare(ONE, TWO)).isEqualTo(TWO.getIntegerProperty().compareTo(ONE.getIntegerProperty())); } @Test // DATACMNS-525 void shouldComparePrimitiveIntegerAscCorrectly() { Comparator comparator = new PropertyPathComparator<>("primitiveProperty"); assertThat(comparator.compare(ONE, TWO)) .isEqualTo(Integer.compare(ONE.getPrimitiveProperty(), TWO.getPrimitiveProperty())); } @Test // DATACMNS-525 void shouldNotFailOnNullValues() { Comparator comparator = new PropertyPathComparator<>("stringProperty"); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(1); } @Test // DATACMNS-525 void shouldComparePrimitiveIntegerDescCorrectly() { Comparator comparator = new PropertyPathComparator("primitiveProperty").desc(); assertThat(comparator.compare(ONE, TWO)) .isEqualTo(Integer.compare(TWO.getPrimitiveProperty(), ONE.getPrimitiveProperty())); } @Test // DATACMNS-525 void shouldSortNullsFirstCorrectly() { Comparator comparator = new PropertyPathComparator("stringProperty").nullsFirst(); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(1); } @Test // DATACMNS-525 void shouldSortNullsLastCorrectly() { Comparator comparator = new PropertyPathComparator("stringProperty").nullsLast(); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(-1); } @Test // DATACMNS-525 void shouldCompareNestedTypesCorrectly() { Comparator comparator = new PropertyPathComparator<>("nestedType.stringProperty"); assertThat(comparator.compare(WRAPPER_ONE, WRAPPER_TWO)).isEqualTo( WRAPPER_ONE.getNestedType().getStringProperty().compareTo(WRAPPER_TWO.getNestedType().getStringProperty())); } @Test // DATACMNS-525 void shouldCompareNestedTypesCorrectlyWhenOneOfThemHasNullValue() { PropertyPathComparator comparator = new PropertyPathComparator<>("nestedType.stringProperty"); assertThat(comparator.compare(WRAPPER_ONE, new WrapperType("two", null))).isGreaterThanOrEqualTo(1); } public static class WrapperType { private String stringPropertyWrapper; private SomeType nestedType; WrapperType(String stringPropertyWrapper, SomeType nestedType) { this.stringPropertyWrapper = stringPropertyWrapper; this.nestedType = nestedType; } public String getStringPropertyWrapper() { return stringPropertyWrapper; } public void setStringPropertyWrapper(String stringPropertyWrapper) { this.stringPropertyWrapper = stringPropertyWrapper; } public SomeType getNestedType() { return nestedType; } public void setNestedType(SomeType nestedType) { this.nestedType = nestedType; } } @SuppressWarnings("WeakerAccess") public static class SomeType { public SomeType() { } SomeType(String stringProperty, Integer integerProperty, int primitiveProperty) { this.stringProperty = stringProperty; this.integerProperty = integerProperty; this.primitiveProperty = primitiveProperty; } String stringProperty; Integer integerProperty; int primitiveProperty; public String getStringProperty() { return stringProperty; } public void setStringProperty(String stringProperty) { this.stringProperty = stringProperty; } public Integer getIntegerProperty() { return integerProperty; } public void setIntegerProperty(Integer integerProperty) { this.integerProperty = integerProperty; } public int getPrimitiveProperty() { return primitiveProperty; } public void setPrimitiveProperty(int primitiveProperty) { this.primitiveProperty = primitiveProperty; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/SpelPropertyComparatorUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import java.util.Comparator; import org.junit.jupiter.api.Test; import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Unit tests for {@link SpelPropertyComparator}. * * @author Christoph Strobl * @author Oliver Gierke * @author Artur Ciocanu */ class SpelPropertyComparatorUnitTests { private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private static final SomeType ONE = new SomeType("one", 1, 1); private static final SomeType TWO = new SomeType("two", 2, 2); private static final WrapperType WRAPPER_ONE = new WrapperType("w-one", ONE); private static final WrapperType WRAPPER_TWO = new WrapperType("w-two", TWO); @Test // DATACMNS-525 void shouldCompareStringAscCorrectly() { Comparator comparator = new SpelPropertyComparator<>("stringProperty", PARSER); assertThat(comparator.compare(ONE, TWO)).isEqualTo(ONE.getStringProperty().compareTo(TWO.getStringProperty())); } @Test // DATACMNS-525 void shouldCompareStringDescCorrectly() { Comparator comparator = new SpelPropertyComparator("stringProperty", PARSER).desc(); assertThat(comparator.compare(ONE, TWO)).isEqualTo(TWO.getStringProperty().compareTo(ONE.getStringProperty())); } @Test // DATACMNS-525 void shouldCompareIntegerAscCorrectly() { Comparator comparator = new SpelPropertyComparator<>("integerProperty", PARSER); assertThat(comparator.compare(ONE, TWO)).isEqualTo(ONE.getIntegerProperty().compareTo(TWO.getIntegerProperty())); } @Test // DATACMNS-525 void shouldCompareIntegerDescCorrectly() { Comparator comparator = new SpelPropertyComparator("integerProperty", PARSER).desc(); assertThat(comparator.compare(ONE, TWO)).isEqualTo(TWO.getIntegerProperty().compareTo(ONE.getIntegerProperty())); } @Test // DATACMNS-525 void shouldComparePrimitiveIntegerAscCorrectly() { Comparator comparator = new SpelPropertyComparator<>("primitiveProperty", PARSER); assertThat(comparator.compare(ONE, TWO)) .isEqualTo(Integer.compare(ONE.getPrimitiveProperty(), TWO.getPrimitiveProperty())); } @Test // DATACMNS-525 void shouldNotFailOnNullValues() { Comparator comparator = new SpelPropertyComparator<>("stringProperty", PARSER); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(1); } @Test // DATACMNS-525 void shouldComparePrimitiveIntegerDescCorrectly() { Comparator comparator = new SpelPropertyComparator("primitiveProperty", PARSER).desc(); assertThat(comparator.compare(ONE, TWO)) .isEqualTo(Integer.compare(TWO.getPrimitiveProperty(), ONE.getPrimitiveProperty())); } @Test // DATACMNS-525 void shouldSortNullsFirstCorrectly() { Comparator comparator = new SpelPropertyComparator("stringProperty", PARSER).nullsFirst(); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(1); } @Test // DATACMNS-525 void shouldSortNullsLastCorrectly() { Comparator comparator = new SpelPropertyComparator("stringProperty", PARSER).nullsLast(); assertThat(comparator.compare(ONE, new SomeType(null, null, 2))).isEqualTo(-1); } @Test // DATACMNS-525 void shouldCompareNestedTypesCorrectly() { Comparator comparator = new SpelPropertyComparator<>("nestedType.stringProperty", PARSER); assertThat(comparator.compare(WRAPPER_ONE, WRAPPER_TWO)).isEqualTo( WRAPPER_ONE.getNestedType().getStringProperty().compareTo(WRAPPER_TWO.getNestedType().getStringProperty())); } @Test // DATACMNS-525 void shouldCompareNestedTypesCorrectlyWhenOneOfThemHasNullValue() { SpelPropertyComparator comparator = new SpelPropertyComparator<>("nestedType.stringProperty", PARSER); assertThat(comparator.compare(WRAPPER_ONE, new WrapperType("two", null))).isGreaterThanOrEqualTo(1); } public static class WrapperType { private String stringPropertyWrapper; private SomeType nestedType; WrapperType(String stringPropertyWrapper, SomeType nestedType) { this.stringPropertyWrapper = stringPropertyWrapper; this.nestedType = nestedType; } public String getStringPropertyWrapper() { return stringPropertyWrapper; } public void setStringPropertyWrapper(String stringPropertyWrapper) { this.stringPropertyWrapper = stringPropertyWrapper; } public SomeType getNestedType() { return nestedType; } public void setNestedType(SomeType nestedType) { this.nestedType = nestedType; } } @SuppressWarnings("WeakerAccess") public static class SomeType { public SomeType() { } SomeType(String stringProperty, Integer integerProperty, int primitiveProperty) { this.stringProperty = stringProperty; this.integerProperty = integerProperty; this.primitiveProperty = primitiveProperty; } String stringProperty; Integer integerProperty; int primitiveProperty; public String getStringProperty() { return stringProperty; } public void setStringProperty(String stringProperty) { this.stringProperty = stringProperty; } public Integer getIntegerProperty() { return integerProperty; } public void setIntegerProperty(Integer integerProperty) { this.integerProperty = integerProperty; } public int getPrimitiveProperty() { return primitiveProperty; } public void setPrimitiveProperty(int primitiveProperty) { this.primitiveProperty = primitiveProperty; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/SpelQueryEngineUnitTests.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.annotation.Id; import org.springframework.data.core.TypeInformation; import org.springframework.data.keyvalue.repository.query.SpelQueryCreator; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.expression.spel.support.SimpleEvaluationContext; /** * Unit tests for {@link SpelQueryEngine}. * * @author Martin Macko * @author Oliver Gierke * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) public class SpelQueryEngineUnitTests { private static final Person BOB_WITH_FIRSTNAME = new Person("bob", 30); private static final Person MIKE_WITHOUT_FIRSTNAME = new Person(null, 25); @Mock KeyValueAdapter adapter; private SpelQueryEngine engine; private Iterable people = Arrays.asList(BOB_WITH_FIRSTNAME, MIKE_WITHOUT_FIRSTNAME); @BeforeEach void setUp() { engine = new SpelQueryEngine(); engine.registerAdapter(adapter); } @Test // DATAKV-114 @SuppressWarnings("unchecked") void queriesEntitiesWithNullProperty() throws Exception { doReturn(people).when(adapter).getAllOf(anyString()); Collection result = engine.execute(createQueryForMethodWithArgs("findByFirstname", "bob"), null, -1, -1, anyString()); assertThat(result).containsExactly(BOB_WITH_FIRSTNAME); } @Test // DATAKV-114 void countsEntitiesWithNullProperty() throws Exception { doReturn(people).when(adapter).getAllOf(anyString()); assertThat(engine.count(createQueryForMethodWithArgs("findByFirstname", "bob"), anyString())).isEqualTo(1L); } private static SpelCriteria createQueryForMethodWithArgs(String methodName, Object... args) throws Exception { List> types = new ArrayList<>(args.length); for (Object arg : args) { types.add(arg.getClass()); } Method method = PersonRepository.class.getMethod(methodName, types.toArray(new Class[types.size()])); RepositoryMetadata metadata = mock(RepositoryMetadata.class); doReturn(method.getReturnType()).when(metadata).getReturnedDomainClass(method); doReturn(TypeInformation.fromReturnTypeOf(method)).when(metadata).getReturnType(method); doReturn(TypeInformation.of(Person.class)).when(metadata).getDomainTypeInformation(); PartTree partTree = new PartTree(method.getName(), method.getReturnType()); SpelQueryCreator creator = new SpelQueryCreator(partTree, new ParametersParameterAccessor( new QueryMethod(method, metadata, new SpelAwareProxyProjectionFactory()).getParameters(), args)); return new SpelCriteria(creator.createQuery().getCriteria(), SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().withRootObject(args).build()); } interface PersonRepository { Person findByFirstname(String firstname); } public static class Person { @Id String id; String firstname; int age; Person(String firstname, int age) { this.firstname = firstname; this.age = age; } public String getFirstname() { return firstname; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/mapping/AnnotationBasedKeySpaceResolverUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import static org.assertj.core.api.Assertions.*; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; import org.springframework.data.keyvalue.TypeWithDirectKeySpaceAnnotation; import org.springframework.data.keyvalue.TypeWithInhteritedPersistentAnnotationNotHavingKeySpace; import org.springframework.data.keyvalue.TypeWithPersistentAnnotationNotHavingKeySpace; import org.springframework.data.keyvalue.annotation.KeySpace; /** * Unit tests for {@link AnnotationBasedKeySpaceResolver}. * * @author Christoph Strobl * @author Oliver Gierke */ class AnnotationBasedKeySpaceResolverUnitTests { private AnnotationBasedKeySpaceResolver resolver; @BeforeEach void setUp() { resolver = AnnotationBasedKeySpaceResolver.INSTANCE; } @Test // DATACMNS-525 void shouldResolveKeySpaceDefaultValueCorrectly() { assertThat(resolver.resolveKeySpace(EntityWithDefaultKeySpace.class)).isEqualTo("daenerys"); } @Test // DATAKV-105 void shouldReturnNullWhenNoKeySpaceFoundOnComposedPersistentAnnotation() { assertThat(resolver.resolveKeySpace(TypeWithInhteritedPersistentAnnotationNotHavingKeySpace.class)).isNull(); } @Test // DATAKV-105 void shouldReturnNullWhenPersistentIsFoundOnNonComposedAnnotation() { assertThat(resolver.resolveKeySpace(TypeWithPersistentAnnotationNotHavingKeySpace.class)).isNull(); } @Test // DATAKV-105 void shouldReturnNullWhenPersistentIsNotFound() { assertThat(resolver.resolveKeySpace(TypeWithoutKeySpace.class)).isNull(); } @Test // DATACMNS-525 void shouldResolveInheritedKeySpaceCorrectly() { assertThat(resolver.resolveKeySpace(EntityWithInheritedKeySpace.class)).isEqualTo("viserys"); } @Test // DATACMNS-525 void shouldResolveDirectKeySpaceAnnotationCorrectly() { assertThat(resolver.resolveKeySpace(TypeWithDirectKeySpaceAnnotation.class)).isEqualTo("rhaegar"); } @Test // DATAKV-129 void shouldResolveKeySpaceUsingAliasForCorrectly() { assertThat(resolver.resolveKeySpace(EntityWithSetKeySpaceUsingAliasFor.class)).isEqualTo("viserys"); } @Test // DATAKV-129 void shouldResolveKeySpaceUsingAliasForCorrectlyOnSubClass() { assertThat(resolver.resolveKeySpace(EntityWithInheritedKeySpaceUsingAliasFor.class)).isEqualTo("viserys"); } @PersistentAnnotationWithExplicitKeySpaceUsingAliasFor private static class EntityWithDefaultKeySpace { } @PersistentAnnotationWithExplicitKeySpaceUsingAliasFor(firstname = "viserys") static class EntityWithSetKeySpaceUsingAliasFor { } private static class EntityWithInheritedKeySpace extends EntityWithSetKeySpaceUsingAliasFor { } private static class EntityWithInheritedKeySpaceUsingAliasFor extends EntityWithSetKeySpaceUsingAliasFor { } @Persistent @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) @KeySpace static @interface PersistentAnnotationWithExplicitKeySpaceUsingAliasFor { @AliasFor(annotation = KeySpace.class, attribute = "value") String firstname() default "daenerys"; String lastname() default "targaryen"; } static class TypeWithoutKeySpace { String foo; } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/mapping/BasicKeyValuePersistentEntityUnitTests.java ================================================ /* * Copyright 2019-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import static org.assertj.core.api.Assertions.*; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.data.keyvalue.annotation.KeySpace; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.mock.env.MockEnvironment; /** * Unit tests for {@link BasicKeyValuePersistentEntity}. * * @author Mark Paluch */ class BasicKeyValuePersistentEntityUnitTests { KeyValueMappingContext, ? extends KeyValuePersistentProperty> mappingContext = new KeyValueMappingContext<>(); @Test // DATAKV-268 void shouldDeriveKeyspaceFromClassName() { assertThat(mappingContext.getPersistentEntity(KeyspaceEntity.class).getRequiredKeySpace()) .isEqualTo(KeyspaceEntity.class.getName()); } @Test // DATAKV-268, GH-613 void shouldEvaluateKeyspaceExpression() { MockEnvironment environment = new MockEnvironment().withProperty("my.property", "foo"); mappingContext.setEnvironment(environment); KeyValuePersistentEntity persistentEntity = mappingContext.getPersistentEntity(ExpressionEntity.class); persistentEntity.setEvaluationContextProvider( new ExtensionAwareEvaluationContextProvider(Collections.singletonList(new SampleExtension()))); assertThat(persistentEntity.getRequiredKeySpace()).isEqualTo("some_foo"); } @Test // DATAKV-268 void shouldEvaluateEntityWithoutKeyspace() { KeyValuePersistentEntity persistentEntity = mappingContext.getPersistentEntity(NoKeyspaceEntity.class); persistentEntity.setEvaluationContextProvider( new ExtensionAwareEvaluationContextProvider(Collections.singletonList(new SampleExtension()))); assertThat(persistentEntity.getRequiredKeySpace()).isEqualTo(NoKeyspaceEntity.class.getName()); } @Test // GH-461 void shouldApplyKeySpaceResolver() { mappingContext.setKeySpaceResolver(new PrefixKeyspaceResolver("foo", ClassNameKeySpaceResolver.INSTANCE)); KeyValuePersistentEntity persistentEntity = mappingContext.getPersistentEntity(NoKeyspaceEntity.class); assertThat(persistentEntity.getKeySpace()).isEqualTo("foo" + NoKeyspaceEntity.class.getName()); } @Test // GH-461 void shouldFallBackToDefaultsIfKeySpaceResolverReturnsNull() { mappingContext.setKeySpaceResolver(it -> null); KeyValuePersistentEntity persistentEntity = mappingContext.getPersistentEntity(NoKeyspaceEntity.class); assertThat(persistentEntity.getKeySpace()).isEqualTo(NoKeyspaceEntity.class.getName()); } @KeySpace("#{myProperty}_${my.property}") private static class ExpressionEntity {} @KeySpace private static class KeyspaceEntity {} private static class NoKeyspaceEntity {} static class SampleExtension implements EvaluationContextExtension { @Override public String getExtensionId() { return "sampleExtension"; } @Override public Map getProperties() { Map properties = new LinkedHashMap<>(); properties.put("myProperty", "some"); return properties; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/mapping/PrefixKeyspaceResolverUnitTests.java ================================================ /* * Copyright 2022-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping; import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; /** * Unit tests for {@link PrefixKeyspaceResolver}. * * @author Mark Paluch */ class PrefixKeyspaceResolverUnitTests { @Test // gh-461 void shouldApplyPrefix() { var resolver = new PrefixKeyspaceResolver("foo", Class::getSimpleName); assertThat(resolver.resolveKeySpace(Object.class)).isEqualTo("fooObject"); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/core/mapping/context/KeyValueMappingContextUnitTests.java ================================================ /* * Copyright 2021-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.core.mapping.context; import static org.assertj.core.api.Assertions.*; import java.math.BigDecimal; import java.math.BigInteger; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; /** * Unit test for {@link KeyValueMappingContext}. * * @author Mark Paluch */ class KeyValueMappingContextUnitTests

> { @Test void shouldNotCreateEntitiesForJavaStandardTypes() { KeyValueMappingContext, P> mappingContext = new KeyValueMappingContext<>(); assertThat(mappingContext.getPersistentEntity(BigInteger.class)).isNull(); assertThat(mappingContext.getPersistentEntity(BigDecimal.class)).isNull(); assertThat(mappingContext.getPersistentEntity(UUID.class)).isNull(); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/MapRepositoriesRegistrarUnitTests.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.map.repository.config.EnableMapRepositories; import org.springframework.data.map.repository.config.MapRepositoriesRegistrar; import org.springframework.data.repository.CrudRepository; /** * @author Christoph Strobl */ class MapRepositoriesRegistrarUnitTests { private BeanDefinitionRegistry registry; @BeforeEach void setUp() { registry = new DefaultListableBeanFactory(); } @ParameterizedTest // GH-499, GH-3440 @MethodSource(value = { "args" }) void configuresRepositoriesCorrectly(AnnotationMetadata metadata, String[] beanNames) { MapRepositoriesRegistrar registrar = new MapRepositoriesRegistrar(); registrar.setResourceLoader(new DefaultResourceLoader()); registrar.setEnvironment(new StandardEnvironment()); registrar.registerBeanDefinitions(metadata, registry); Iterable names = Arrays.asList(registry.getBeanDefinitionNames()); assertThat(names).contains(beanNames); } static Stream args() { return Stream.of( Arguments.of(AnnotationMetadata.introspect(Config.class), new String[] { "mapRepositoriesRegistrarUnitTests.PersonRepository" }), Arguments.of(AnnotationMetadata.introspect(ConfigWithBeanNameGenerator.class), new String[] { "mapRepositoriesRegistrarUnitTests.PersonREPO" })); } @EnableMapRepositories(basePackageClasses = PersonRepository.class, considerNestedRepositories = true) private class Config { } @EnableMapRepositories(basePackageClasses = PersonRepository.class, nameGenerator = MyBeanNameGenerator.class, considerNestedRepositories = true) private class ConfigWithBeanNameGenerator { } static class MyBeanNameGenerator extends AnnotationBeanNameGenerator { @Override public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { return super.generateBeanName(definition, registry).replaceAll("Repository", "REPO"); } } interface PersonRepository extends CrudRepository { } static class Person {} } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/SimpleKeyValueRepositoryUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Persistent; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; /** * @author Christoph Strobl * @author Eugene Nikiforov * @author Jens Schauder * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) class SimpleKeyValueRepositoryUnitTests { private SimpleKeyValueRepository repo; private @Mock KeyValueOperations opsMock; private KeyValueMappingContext context; @BeforeEach void setUp() { this.context = new KeyValueMappingContext<>(); EntityInformation ei = getEntityInformationFor(Foo.class); repo = new SimpleKeyValueRepository<>(ei, opsMock); } @Test // DATACMNS-525 void saveNewWithNumericId() { EntityInformation ei = getEntityInformationFor(WithNumericId.class); SimpleKeyValueRepository temp = new SimpleKeyValueRepository<>(ei, opsMock); WithNumericId withNumericId = new WithNumericId(); temp.save(withNumericId); verify(opsMock, times(1)).insert(eq(withNumericId)); } @Test // DATACMNS-525 void testDoubleSave() { Foo foo = new Foo("one"); repo.save(foo); foo.id = "1"; repo.save(foo); verify(opsMock, times(1)).insert(eq(foo)); verify(opsMock, times(1)).update(eq(foo.getId()), eq(foo)); } @Test // DATACMNS-525 void multipleSave() { Foo one = new Foo("one"); Foo two = new Foo("two"); repo.saveAll(Arrays.asList(one, two)); verify(opsMock, times(1)).insert(eq(one)); verify(opsMock, times(1)).insert(eq(two)); } @Test // DATACMNS-525 void deleteEntity() { Foo one = new Foo("one"); one.id = "1"; repo.save(one); repo.delete(one); verify(opsMock, times(1)).delete(eq(one)); } @Test // DATACMNS-525 void deleteById() { repo.deleteById("one"); verify(opsMock, times(1)).delete(eq("one"), eq(Foo.class)); } @Test // DATAKV-330 void deleteAllById() { repo.deleteAllById(Arrays.asList("one", "two")); verify(opsMock, times(1)).delete(eq("one"), eq(Foo.class)); verify(opsMock, times(1)).delete(eq("two"), eq(Foo.class)); } @Test // DATACMNS-525 void deleteAll() { repo.deleteAll(); verify(opsMock, times(1)).delete(eq(Foo.class)); } @Test // DATACMNS-525 @SuppressWarnings("unchecked") void findAllIds() { when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.empty()); repo.findAllById(Arrays.asList("one", "two", "three")); verify(opsMock, times(3)).findById(anyString(), eq(Foo.class)); } @Test // DATAKV-186 @SuppressWarnings("unchecked") void existsByIdReturnsFalseForEmptyOptional() { when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.empty()); assertThat(repo.existsById("one")).isFalse(); } @Test // DATAKV-186 @SuppressWarnings("unchecked") void existsByIdReturnsTrueWhenOptionalValuePresent() { when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.of(new Foo())); assertThat(repo.existsById("one")).isTrue(); } @Test // DATACMNS-525 void findAllWithPageableShouldDelegateToOperationsCorrectlyWhenPageableDoesNotContainSort() { repo.findAll(PageRequest.of(10, 15)); verify(opsMock, times(1)).findInRange(eq(150L), eq(15), eq(Sort.unsorted()), eq(Foo.class)); } @Test // DATACMNS-525 void findAllWithPageableShouldDelegateToOperationsCorrectlyWhenPageableContainsSort() { Sort sort = Sort.by("for", "bar"); repo.findAll(PageRequest.of(10, 15, sort)); verify(opsMock, times(1)).findInRange(eq(150L), eq(15), eq(sort), eq(Foo.class)); } @Test // DATACMNS-525 void findAllShouldFallbackToFindAllOfWhenGivenNullPageable() { repo.findAll(Pageable.unpaged()); verify(opsMock, times(1)).findAll(eq(Foo.class)); } @SuppressWarnings("unchecked") private EntityInformation getEntityInformationFor(Class type) { PersistentEntity requiredPersistentEntity = (PersistentEntity) context .getRequiredPersistentEntity(type); return new PersistentEntityInformation<>(requiredPersistentEntity); } static class Foo { private @Id String id; private Long longValue; private String name; private Bar bar; Foo(String name) { this.name = name; } public Foo() {} public String getId() { return this.id; } public Long getLongValue() { return this.longValue; } public String getName() { return this.name; } public Bar getBar() { return this.bar; } public void setId(String id) { this.id = id; } public void setLongValue(Long longValue) { this.longValue = longValue; } public void setName(String name) { this.name = name; } public void setBar(Bar bar) { this.bar = bar; } } private static class Bar { private String bar; public String getBar() { return this.bar; } public void setBar(String bar) { this.bar = bar; } } @Persistent static class WithNumericId { @Id Integer id; } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.core.TypeInformation; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl * @author Tom Van Wemmel */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public abstract class AbstractQueryCreatorTestBase, ?>, CRITERIA> { static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME; static final Person RICKON = new Person("rickon", 4); static final Person BRAN = new Person("bran", 9)// .skinChanger(true).bornAt(Date.from(ZonedDateTime.parse("2013-01-31T06:00:00Z", FORMATTER).toInstant())); static final Person ARYA = new Person("arya", 13); static final Person ROBB = new Person("robb", 16)// .named("stark").bornAt(Date.from(ZonedDateTime.parse("2010-09-20T06:00:00Z", FORMATTER).toInstant())); static final Person JON = new Person("jon", 17).named("snow"); @Mock RepositoryMetadata metadataMock; @Test // DATACMNS-525 void equalsReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstname", BRAN.firstname).against(BRAN)).isTrue(); } @Test // DATACMNS-525 void equalsReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstname", BRAN.firstname).against(RICKON)).isFalse(); } @Test // GH-603 void notEqualsReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameNot", BRAN.firstname).against(RICKON)).isTrue(); } @Test // GH-603 void notEqualsReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameNot", BRAN.firstname).against(BRAN)).isFalse(); } @Test // DATACMNS-525 void isTrueAssertedProperlyWhenTrue() { assertThat(evaluate("findBySkinChangerIsTrue").against(BRAN)).isTrue(); } @Test // DATACMNS-525 void isTrueAssertedProperlyWhenFalse() { assertThat(evaluate("findBySkinChangerIsTrue").against(RICKON)).isFalse(); } @Test // DATACMNS-525 void isFalseAssertedProperlyWhenTrue() { assertThat(evaluate("findBySkinChangerIsFalse").against(BRAN)).isFalse(); } @Test // DATACMNS-525 void isFalseAssertedProperlyWhenFalse() { assertThat(evaluate("findBySkinChangerIsFalse").against(RICKON)).isTrue(); } @Test // DATACMNS-525 void isNullAssertedProperlyWhenAttributeIsNull() { assertThat(evaluate("findByLastnameIsNull").against(BRAN)).isTrue(); } @Test // DATACMNS-525 void isNullAssertedProperlyWhenAttributeIsNotNull() { assertThat(evaluate("findByLastnameIsNull").against(ROBB)).isFalse(); } @Test // DATACMNS-525 void isNotNullFalseTrueWhenAttributeIsNull() { assertThat(evaluate("findByLastnameIsNotNull").against(BRAN)).isFalse(); } @Test // DATACMNS-525 void isNotNullReturnsTrueAttributeIsNotNull() { assertThat(evaluate("findByLastnameIsNotNull").against(ROBB)).isTrue(); } @Test // DATACMNS-525 void startsWithReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameStartingWith", "r").against(ROBB)).isTrue(); } @Test // DATACMNS-525 void startsWithReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameStartingWith", "r").against(BRAN)).isFalse(); } @Test // DATACMNS-525 void likeReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameLike", "ob").against(ROBB)).isTrue(); } @Test // DATACMNS-525 void likeReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameLike", "ra").against(ROBB)).isFalse(); } @Test // GH-603 void notLikeReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameNotLike", "ra").against(ROBB)).isTrue(); } @Test // GH-603 void notLikeReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameNotLike", "ob").against(ROBB)).isFalse(); } @Test // DATACMNS-525 void endsWithReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameEndingWith", "bb").against(ROBB)).isTrue(); } @Test // DATACMNS-525 void endsWithReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameEndingWith", "an").against(ROBB)).isFalse(); } @Test // DATACMNS-525 void startsWithIgnoreCaseReturnsTrueWhenMatching() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) .isThrownBy(() -> evaluate("findByFirstnameIgnoreCase", "R").against(ROBB)); } @Test // DATACMNS-525 void greaterThanReturnsTrueForHigherValues() { assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(ROBB)).isTrue(); } @Test // DATACMNS-525 void greaterThanReturnsFalseForLowerValues() { assertThat(evaluate("findByAgeGreaterThan", BRAN.age).against(RICKON)).isFalse(); } @Test // DATACMNS-525 void afterReturnsTrueForHigherValues() { assertThat(evaluate("findByBirthdayAfter", ROBB.birthday).against(BRAN)).isTrue(); } @Test // DATACMNS-525 void afterReturnsFalseForLowerValues() { assertThat(evaluate("findByBirthdayAfter", BRAN.birthday).against(ROBB)).isFalse(); } @Test // DATACMNS-525 void greaterThanEaualsReturnsTrueForHigherValues() { assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(ROBB)).isTrue(); } @Test // DATACMNS-525 void greaterThanEqualsReturnsTrueForEqualValues() { assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(BRAN)).isTrue(); } @Test // DATACMNS-525 void greaterThanEqualsReturnsFalseForLowerValues() { assertThat(evaluate("findByAgeGreaterThanEqual", BRAN.age).against(RICKON)).isFalse(); } @Test // DATACMNS-525 void lessThanReturnsTrueForHigherValues() { assertThat(evaluate("findByAgeLessThan", BRAN.age).against(ROBB)).isFalse(); } @Test // DATACMNS-525 void lessThanReturnsFalseForLowerValues() { assertThat(evaluate("findByAgeLessThan", BRAN.age).against(RICKON)).isTrue(); } @Test // DATACMNS-525 void beforeReturnsTrueForLowerValues() { assertThat(evaluate("findByBirthdayBefore", BRAN.birthday).against(ROBB)).isTrue(); } @Test // DATACMNS-525 void beforeReturnsFalseForHigherValues() { assertThat(evaluate("findByBirthdayBefore", ROBB.birthday).against(BRAN)).isFalse(); } @Test // DATACMNS-525 void lessThanEaualsReturnsTrueForHigherValues() { assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(ROBB)).isFalse(); } @Test // DATACMNS-525 void lessThanEaualsReturnsTrueForEqualValues() { assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(BRAN)).isTrue(); } @Test // DATACMNS-525 void lessThanEqualsReturnsFalseForLowerValues() { assertThat(evaluate("findByAgeLessThanEqual", BRAN.age).against(RICKON)).isTrue(); } @Test // DATACMNS-525 void betweenEqualsReturnsTrueForValuesInBetween() { assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(ARYA)).isTrue(); } @Test // DATACMNS-525 void betweenEqualsReturnsFalseForHigherValues() { assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(JON)).isFalse(); } @Test // DATACMNS-525 void betweenEqualsReturnsFalseForLowerValues() { assertThat(evaluate("findByAgeBetween", BRAN.age, ROBB.age).against(RICKON)).isFalse(); } @Test // DATACMNS-525 void connectByAndReturnsTrueWhenAllPropertiesMatching() { assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(JON)).isTrue(); } @Test // DATACMNS-525 void connectByAndReturnsFalseWhenOnlyFewPropertiesMatch() { assertThat(evaluate("findByAgeGreaterThanAndLastname", BRAN.age, JON.lastname).against(ROBB)).isFalse(); } @Test // DATACMNS-525 void connectByOrReturnsTrueWhenOnlyFewPropertiesMatch() { assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(ROBB)).isTrue(); } @Test // DATACMNS-525 void connectByOrReturnsTrueWhenAllPropertiesMatch() { assertThat(evaluate("findByAgeGreaterThanOrLastname", BRAN.age, JON.lastname).against(JON)).isTrue(); } @Test // DATACMNS-525 void regexReturnsTrueWhenMatching() { assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(JON)).isTrue(); } @Test // DATACMNS-525 void regexReturnsFalseWhenNotMatching() { assertThat(evaluate("findByLastnameMatches", "^s.*w$").against(ROBB)).isFalse(); } @Test // DATAKV-169 void inReturnsMatchCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameIn", list).against(ROBB)).isTrue(); } @Test // DATAKV-169 void inNotMatchingReturnsCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); } @Test // DATAKV-169 void inWithNullCompareValuesCorrectly() { List list = new ArrayList<>(); list.add(null); assertThat(evaluate("findByFirstnameIn", list).against(JON)).isFalse(); } @Test // DATAKV-169 void inWithNullSourceValuesMatchesCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) .isFalse(); } @Test // DATAKV-169 void inMatchesNullValuesCorrectly() { List list = new ArrayList<>(); list.add(null); boolean contains = list.contains(null); assertThat(evaluate("findByFirstnameIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) .isTrue(); } @Test // GH-603 void notInReturnsMatchCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameNotIn", list).against(JON)).isTrue(); } @Test // GH-603 void notInNotMatchingReturnsCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameNotIn", list).against(ROBB)).isFalse(); } @Test // GH-603 void notInWithNullCompareValuesCorrectly() { List list = new ArrayList<>(); list.add(null); assertThat(evaluate("findByFirstnameNotIn", list).against(JON)).isTrue(); } @Test // GH-603 void notInWithNullSourceValuesMatchesCorrectly() { List list = new ArrayList<>(); list.add(ROBB.firstname); assertThat(evaluate("findByFirstnameNotIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) .isTrue(); } @Test // GH-603 void notInMatchesNullValuesCorrectly() { List list = new ArrayList<>(); list.add(null); assertThat(evaluate("findByFirstnameNotIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) .isFalse(); } @Test // DATAKV-185 void noDerivedQueryArgumentsMatchesAlways() { assertThat(evaluate("findBy").against(JON)).isTrue(); assertThat(evaluate("findBy").against(null)).isTrue(); } protected Evaluation evaluate(String methodName, Object... args) { try { return createEvaluation(createQueryForMethodWithArgs(methodName, args).getCriteria()); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } protected abstract Evaluation createEvaluation(CRITERIA criteria); protected KeyValueQuery createQueryForMethodWithArgs(String methodName, Object... args) throws NoSuchMethodException, SecurityException { Class[] argTypes = new Class[args.length]; if (!ObjectUtils.isEmpty(args)) { for (int i = 0; i < args.length; i++) { argTypes[i] = args[i].getClass(); } } Method method = getMethod(PersonRepository.class, methodName, argTypes); doReturn(Person.class).when(metadataMock).getReturnedDomainClass(method); doReturn(TypeInformation.of(Person.class)).when(metadataMock).getDomainTypeInformation(); doReturn(TypeInformation.of(Person.class)).when(metadataMock).getReturnType(method); PartTree partTree = new PartTree(method.getName(), method.getReturnType()); QUERY_CREATOR creator = queryCreator(partTree, new ParametersParameterAccessor( new QueryMethod(method, metadataMock, new SpelAwareProxyProjectionFactory()).getParameters(), args)); KeyValueQuery q = creator.createQuery(); return finalizeQuery(q, args); } private Method getMethod(Class type, String methodName, Class[] argTypes) throws NoSuchMethodException { for (Method declaredMethod : type.getDeclaredMethods()) { if (!declaredMethod.getName().equals(methodName)) { continue; } if (declaredMethod.getParameterCount() != argTypes.length) { continue; } Class[] types = declaredMethod.getParameterTypes(); boolean assigable = true; for (int i = 0; i < types.length; i++) { if (!types[i].isAssignableFrom(argTypes[i])) { assigable = false; break; } } if (assigable) { return declaredMethod; } } throw new NoSuchMethodException("Method " + methodName + " not found in " + type); } protected abstract QUERY_CREATOR queryCreator(PartTree partTree, ParametersParameterAccessor accessor); protected abstract KeyValueQuery finalizeQuery(KeyValueQuery query, Object... args); interface PersonRepository extends CrudRepository { // No arguments Person findBy(); // Type.SIMPLE_PROPERTY Person findByFirstname(String firstname); // Type.NEGATING_SIMPLE_PROPERTY Person findByFirstnameNot(String firstname); // Type.TRUE Person findBySkinChangerIsTrue(); // Type.FALSE Person findBySkinChangerIsFalse(); // Type.IS_NULL Person findByLastnameIsNull(); // Type.IS_NOT_NULL Person findByLastnameIsNotNull(); // Type.STARTING_WITH Person findByFirstnameStartingWith(String firstanme); Person findByFirstnameIgnoreCase(String firstanme); // Type.AFTER Person findByBirthdayAfter(Date date); // Type.GREATHER_THAN Person findByAgeGreaterThan(Integer age); // Type.GREATER_THAN_EQUAL Person findByAgeGreaterThanEqual(Integer age); // Type.BEFORE Person findByBirthdayBefore(Date date); // Type.LESS_THAN Person findByAgeLessThan(Integer age); // Type.LESS_THAN_EQUAL Person findByAgeLessThanEqual(Integer age); // Type.BETWEEN Person findByAgeBetween(Integer low, Integer high); // Type.LIKE Person findByFirstnameLike(String firstname); // Type.NOT_LIKE Person findByFirstnameNotLike(String firstname); // Type.ENDING_WITH Person findByFirstnameEndingWith(String firstname); Person findByAgeGreaterThanAndLastname(Integer age, String lastname); Person findByAgeGreaterThanOrLastname(Integer age, String lastname); // Type.REGEX Person findByLastnameMatches(String lastname); // Type.IN Person findByFirstnameIn(List in); // Type.NOT_IN Person findByFirstnameNotIn(List in); } public interface Evaluation { Boolean against(Object candidate); boolean evaluate(); } public static class Person { private @Id String id; private String firstname, lastname; private int age; private boolean isSkinChanger = false; private Date birthday; public Person() {} Person(String firstname, int age) { super(); this.firstname = firstname; this.age = age; } Person skinChanger(boolean isSkinChanger) { this.isSkinChanger = isSkinChanger; return this; } Person named(String lastname) { this.lastname = lastname; return this; } Person bornAt(Date date) { this.birthday = date; return this; } public String getId() { return this.id; } public String getFirstname() { return this.firstname; } public String getLastname() { return this.lastname; } public int getAge() { return this.age; } public boolean isSkinChanger() { return this.isSkinChanger; } public Date getBirthday() { return this.birthday; } public void setId(String id) { this.id = id; } public void setFirstname(String firstname) { this.firstname = firstname; } public void setLastname(String lastname) { this.lastname = lastname; } public void setAge(int age) { this.age = age; } public void setSkinChanger(boolean isSkinChanger) { this.isSkinChanger = isSkinChanger; } public void setBirthday(Date birthday) { this.birthday = birthday; } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/query/CachingKeyValuePartTreeQueryUnitTests.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.core.TypeInformation; import org.springframework.data.keyvalue.Person; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.SpelCriteria; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Unit tests for {@link CachingKeyValuePartTreeQuery}. * * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) class CachingKeyValuePartTreeQueryUnitTests { @Mock KeyValueOperations kvOpsMock; @Mock RepositoryMetadata metadataMock; @Mock ProjectionFactory projectionFactoryMock; @BeforeEach @SuppressWarnings({ "unchecked", "rawtypes" }) void setUp() throws Exception { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); when(metadataMock.getReturnType(any(Method.class))).thenReturn(TypeInformation.of((Class) List.class)); } @Test // DATAKV-137 void cachedSpelExpressionShouldBeReusedWithNewContext() throws NoSuchMethodException, SecurityException { QueryMethod qm = new QueryMethod(Repo.class.getMethod("findByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery query = new CachingKeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); Object[] args = new Object[] { "foo" }; SpelCriteria first = (SpelCriteria) query.prepareQuery(args).getCriteria(); SpelCriteria second = (SpelCriteria) query.prepareQuery(args).getCriteria(); assertThat(first.getExpression()).isSameAs(second.getExpression()); assertThat(first.getContext()).isNotSameAs(second.getContext()); } static interface Repo { List findByFirstname(String firstname); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQueryUnitTests.java ================================================ /* * Copyright 2015-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.keyvalue.Person; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.SpelCriteria; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Unit tests for {@link KeyValuePartTreeQuery}. * * @author Christoph Strobl * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) class KeyValuePartTreeQueryUnitTests { @Mock KeyValueOperations kvOpsMock; @Mock RepositoryMetadata metadataMock; @Mock ProjectionFactory projectionFactoryMock; @Test // DATAKV-115 @SuppressWarnings({ "unchecked", "rawtypes" }) void spelExpressionAndContextShouldNotBeReused() throws NoSuchMethodException, SecurityException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(List.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("findByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery query = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); Object[] args = new Object[] { "foo" }; Object first = query.prepareQuery(args).getCriteria(); Object second = query.prepareQuery(args).getCriteria(); assertThat(first).isNotSameAs(second); } @Test // DATAKV-142 @SuppressWarnings({ "unchecked", "rawtypes" }) void shouldApplyPageableParameterToCollectionQuery() throws SecurityException, NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(List.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("findBy", Pageable.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { PageRequest.of(2, 3) }); assertThat(query.getOffset()).isEqualTo(6L); assertThat(query.getRows()).isEqualTo(3); } @Test // GH-563 @SuppressWarnings({ "unchecked", "rawtypes" }) void shouldAllowProjectionQueries() throws SecurityException, NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(List.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("findProjectionByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { "firstname" }); partTreeQuery.doExecute(new Object[] { "firstname" }, query); verify(kvOpsMock).find(eq(query), eq(Person.class)); } @Test // DATAKV-142 @SuppressWarnings({ "unchecked", "rawtypes" }) void shouldApplyDerivedMaxResultsToQuery() throws SecurityException, NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(List.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("findTop3By"), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] {}); assertThat(query.getRows()).isEqualTo(3); } @Test // DATAKV-142 @SuppressWarnings({ "unchecked", "rawtypes" }) void shouldApplyDerivedMaxResultsToQueryWithParameters() throws SecurityException, NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(List.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("findTop3ByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { "firstname" }); assertThat(query.getCriteria()).isInstanceOf(SpelCriteria.class); assertThat(((SpelCriteria) query.getCriteria()).getExpression().getExpressionString()) .isEqualTo("#it?.firstname?.equals([0])"); assertThat(query.getRows()).isEqualTo(3); } @Test // GH-385 void shouldUseCountForExists() throws NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(Boolean.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Boolean.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("existsByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { "firstname" }); partTreeQuery.doExecute(new Object[] { "firstname" }, query); verify(kvOpsMock).exists(eq(query), eq(Person.class)); } @Test // GH-71 void shouldUseCountForCount() throws NoSuchMethodException { when(metadataMock.getDomainType()).thenReturn((Class) Person.class); when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(Boolean.class)); when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Boolean.class); QueryMethod qm = new QueryMethod(Repo.class.getMethod("countByFirstname", String.class), metadataMock, projectionFactoryMock); KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, SpelQueryCreator.class); KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { "firstname" }); partTreeQuery.doExecute(new Object[] { "firstname" }, query); verify(kvOpsMock).count(eq(query), eq(Person.class)); } interface Repo { List findByFirstname(String firstname); boolean existsByFirstname(String firstname); int countByFirstname(String firstname); List findBy(Pageable page); List findTop3By(); List findTop3ByFirstname(String firstname); PersonProjection findProjectionByFirstname(String firstname); } interface PersonProjection { String getFirstname(); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreatorUnitTests.java ================================================ /* * Copyright 2024-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import static org.assertj.core.api.Assertions.*; import java.util.function.Predicate; import org.junit.jupiter.api.Test; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; /** * @author Christoph Strobl */ class PredicateQueryCreatorUnitTests extends AbstractQueryCreatorTestBase> { @Override @Test // DATACMNS-525 void startsWithIgnoreCaseReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameIgnoreCase", "RobB").against(ROBB)).isTrue(); } @Override protected PredicateQueryCreator queryCreator(PartTree partTree, ParametersParameterAccessor accessor) { return new PredicateQueryCreator(partTree, accessor); } @Override protected KeyValueQuery> finalizeQuery(KeyValueQuery> query, Object... args) { return query; } @Override protected Evaluation createEvaluation(Predicate predicate) { return new PredicateEvaluation(predicate); } static class PredicateEvaluation implements Evaluation { private final Predicate expression; private Object candidate; PredicateEvaluation(Predicate expression) { this.expression = expression; } public Boolean against(Object candidate) { this.candidate = candidate; return evaluate(); } public boolean evaluate() { return expression.test(candidate); } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreatorUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.query; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.support.SimpleEvaluationContext; /** * @author Christoph Strobl * @author Mark Paluch */ public class SpelQueryCreatorUnitTests extends AbstractQueryCreatorTestBase { @Override protected SpelQueryCreator queryCreator(PartTree partTree, ParametersParameterAccessor accessor) { return new SpelQueryCreator(partTree, accessor); } @Override protected KeyValueQuery finalizeQuery(KeyValueQuery query, Object... args) { query.getCriteria().setEvaluationContext( SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(args).withInstanceMethods().build()); return query; } @Override protected Evaluation createEvaluation(SpelExpression spelExpression) { return new SpelEvaluation(spelExpression); } static class SpelEvaluation implements Evaluation { SpelExpression expression; Object candidate; SpelEvaluation(SpelExpression expression) { this.expression = expression; } public Boolean against(Object candidate) { this.candidate = candidate; return evaluate(); } public boolean evaluate() { expression.getEvaluationContext().setVariable("it", candidate); return expression.getValue(Boolean.class); } } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/support/KeyValueQuerydslUtilsUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.keyvalue.repository.support.KeyValueQuerydslUtils.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.keyvalue.Person; import org.springframework.data.keyvalue.QPerson; import org.springframework.data.querydsl.SimpleEntityPathResolver; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.PathBuilder; /** * Unit tests for {@link KeyValueQuerydslUtils}. * * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke * @author Mark Paluch */ class KeyValueQuerydslUtilsUnitTests { private EntityPath path; private PathBuilder builder; @BeforeEach void setUp() { this.path = SimpleEntityPathResolver.INSTANCE.createPath(Person.class); this.builder = new PathBuilder<>(path.getType(), path.getMetadata()); } @Test // DATACMNS-525 void toOrderSpecifierThrowsExceptioOnNullPathBuilder() { assertThatIllegalArgumentException().isThrownBy(() -> toOrderSpecifier(Sort.by("firstname"), null)); } @Test // DATACMNS-525, DATAKV-197 void toOrderSpecifierReturnsEmptyArrayWhenSortIsUnsorted() { assertThat(toOrderSpecifier(Sort.unsorted(), builder)).hasSize(0); } @Test // DATACMNS-525 void toOrderSpecifierConvertsSimpleAscSortCorrectly() { Sort sort = Sort.by(Direction.ASC, "firstname"); OrderSpecifier[] specifiers = toOrderSpecifier(sort, builder); assertThat(specifiers).containsExactly(QPerson.person.firstname.asc()); } @Test // DATACMNS-525 void toOrderSpecifierConvertsSimpleDescSortCorrectly() { Sort sort = Sort.by(Direction.DESC, "firstname"); OrderSpecifier[] specifiers = toOrderSpecifier(sort, builder); assertThat(specifiers).containsExactly(QPerson.person.firstname.desc()); } @Test // DATACMNS-525 void toOrderSpecifierConvertsSortCorrectlyAndRetainsArgumentOrder() { Sort sort = Sort.by(Direction.DESC, "firstname").and(Sort.by(Direction.ASC, "age")); OrderSpecifier[] specifiers = toOrderSpecifier(sort, builder); assertThat(specifiers).containsExactly(QPerson.person.firstname.desc(), QPerson.person.age.asc()); } @Test // DATACMNS-525 void toOrderSpecifierConvertsSortWithNullHandlingCorrectly() { Sort sort = Sort.by(new Sort.Order(Direction.DESC, "firstname", NullHandling.NULLS_LAST)); OrderSpecifier[] specifiers = toOrderSpecifier(sort, builder); assertThat(specifiers).containsExactly(QPerson.person.firstname.desc().nullsLast()); } } ================================================ FILE: src/test/java/org/springframework/data/keyvalue/repository/support/KeyValueRepositoryFactoryBeanUnitTests.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.keyvalue.repository.support; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.AbstractQueryCreator; /** * Unit tests for {@link KeyValueRepositoryFactoryBean}. * * @author Oliver Gierke * @author Mark Paluch */ class KeyValueRepositoryFactoryBeanUnitTests { private KeyValueRepositoryFactoryBean factoryBean; @BeforeEach void setUp() { this.factoryBean = new KeyValueRepositoryFactoryBean, Object, Object>( SampleRepository.class); } @Test // DATAKV-123 void rejectsNullKeyValueOperations() { assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.setKeyValueOperations(null)); } @Test // DATAKV-123 void rejectsNullQueryCreator() { assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.setQueryCreator(null)); } @Test // DATAKV-123 void rejectsUninitializedInstance() { assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.afterPropertiesSet()); } @SuppressWarnings("unchecked") @Test // DATAKV-123 void rejectsInstanceWithoutKeyValueOperations() { Class> creatorType = (Class>) mock( AbstractQueryCreator.class).getClass(); factoryBean.setQueryCreator(creatorType); assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.afterPropertiesSet()); } @Test // DATAKV-123 void rejectsInstanceWithoutQueryCreator() { factoryBean.setKeyValueOperations(mock(KeyValueOperations.class)); assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.afterPropertiesSet()); } @Test // DATAKV-123 @SuppressWarnings("unchecked") void createsRepositoryFactory() { Class> creatorType = (Class>) mock( AbstractQueryCreator.class).getClass(); Class queryType = mock(KeyValuePartTreeQuery.class).getClass(); factoryBean.setQueryCreator(creatorType); factoryBean.setKeyValueOperations(mock(KeyValueOperations.class)); factoryBean.setQueryType(queryType); assertThat(factoryBean.createRepositoryFactory()).isNotNull(); } @Test // DATAKV-112 void rejectsNullQueryType() { assertThatIllegalArgumentException().isThrownBy(() -> factoryBean.setQueryType(null)); } interface SampleRepository extends Repository {} } ================================================ FILE: src/test/java/org/springframework/data/map/AbstractRepositoryUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.keyvalue.Person; import org.springframework.data.keyvalue.QPerson; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; import org.springframework.data.repository.CrudRepository; /** * Base class for test cases for repository implementations. * * @author Christoph Strobl * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch * @author Jens Schauder */ public abstract class AbstractRepositoryUnitTests { static final Person CERSEI = new Person("cersei", 19); static final Person JAIME = new Person("jaime", 19); static final Person TYRION = new Person("tyrion", 17); static List LENNISTERS = Arrays.asList(CERSEI, JAIME, TYRION); protected final QPerson person = QPerson.person; protected T repository; @BeforeEach void setup() { KeyValueOperations operations = new KeyValueTemplate(new MapKeyValueAdapter()); KeyValueRepositoryFactory keyValueRepositoryFactory = createKeyValueRepositoryFactory(operations); this.repository = getRepository(keyValueRepositoryFactory); } @Test // DATACMNS-525 void findBy() { repository.saveAll(LENNISTERS); assertThat(repository.findByAge(19)).contains(CERSEI, JAIME); } @Test // DATAKV-137 void findByFirstname() { repository.saveAll(LENNISTERS); assertThat(repository.findByFirstname(CERSEI.getFirstname())).contains(CERSEI); assertThat(repository.findByFirstname(JAIME.getFirstname())).contains(JAIME); } @Test // DATACMNS-525, DATAKV-137 void combindedFindUsingAnd() { repository.saveAll(LENNISTERS); assertThat(repository.findByFirstnameAndAge(JAIME.getFirstname(), 19)).contains(JAIME); assertThat(repository.findByFirstnameAndAge(TYRION.getFirstname(), 17)).contains(TYRION); } @Test // DATACMNS-525 void findPage() { repository.saveAll(LENNISTERS); Page page = repository.findByAge(19, PageRequest.of(0, 1)); assertThat(page.hasNext()).isTrue(); assertThat(page.getTotalElements()).isEqualTo(2L); assertThat(page.getContent()).hasSize(1); Page next = repository.findByAge(19, page.nextPageable()); assertThat(next.hasNext()).isFalse(); assertThat(next.getTotalElements()).isEqualTo(2L); assertThat(next.getContent()).hasSize(1); } @Test // DATACMNS-525 void findByConnectingOr() { repository.saveAll(LENNISTERS); assertThat(repository.findByAgeOrFirstname(19, TYRION.getFirstname())).contains(CERSEI, JAIME, TYRION); } @Test // DATACMNS-525, DATAKV-137 void singleEntityExecution() { repository.saveAll(LENNISTERS); assertThat(repository.findByAgeAndFirstname(TYRION.getAge(), TYRION.getFirstname())).isEqualTo(TYRION); assertThat(repository.findByAgeAndFirstname(CERSEI.getAge(), CERSEI.getFirstname())).isEqualTo(CERSEI); } @Test // DATACMNS-525 void findAllShouldRespectSort() { repository.saveAll(LENNISTERS); assertThat( repository.findAll(Sort.by(new Sort.Order(Direction.ASC, "age"), new Sort.Order(Direction.DESC, "firstname")))) .containsExactly(TYRION, JAIME, CERSEI); } @Test // DATACMNS-525 void derivedFinderShouldRespectSort() { repository.saveAll(LENNISTERS); List result = repository.findByAgeGreaterThanOrderByAgeAscFirstnameDesc(2); assertThat(result).containsExactly(TYRION, JAIME, CERSEI); } @Test // DATAKV-121 void projectsResultToInterface() { repository.saveAll(LENNISTERS); List result = repository.findByAgeGreaterThan(0, Sort.by("firstname")); assertThat(result).hasSize(3); assertThat(result.get(0).getFirstname()).isEqualTo(CERSEI.getFirstname()); } @Test // DATAKV-121 void projectsResultToDynamicInterface() { repository.saveAll(LENNISTERS); List result = repository.findByAgeGreaterThan(0, Sort.by("firstname"), PersonSummary.class); assertThat(result).hasSize(3); assertThat(result.get(0).getFirstname()).isEqualTo(CERSEI.getFirstname()); } @Test // DATAKV-169 void findsByValueInCollectionCorrectly() { repository.saveAll(LENNISTERS); List result = repository.findByFirstnameIn(Arrays.asList(CERSEI.getFirstname(), JAIME.getFirstname())); assertThat(result).hasSize(2); assertThat(result).contains(CERSEI, JAIME); } @Test // DATAKV-169 void findsByValueInCollectionCorrectlyWhenTargetPathContainsNullValue() { repository.saveAll(LENNISTERS); repository.save(new Person(null, 10)); List result = repository.findByFirstnameIn(Arrays.asList(CERSEI.getFirstname(), JAIME.getFirstname())); assertThat(result).hasSize(2); assertThat(result).contains(CERSEI, JAIME); } @Test // DATAKV-169 void findsByValueInCollectionCorrectlyWhenTargetPathAndCollectionContainNullValue() { repository.saveAll(LENNISTERS); Person personWithNullAsFirstname = new Person(null, 10); repository.save(personWithNullAsFirstname); List result = repository .findByFirstnameIn(Arrays.asList(CERSEI.getFirstname(), JAIME.getFirstname(), null)); assertThat(result).hasSize(3); assertThat(result).contains(CERSEI, JAIME, personWithNullAsFirstname); } protected KeyValueRepositoryFactory createKeyValueRepositoryFactory(KeyValueOperations operations) { return new KeyValueRepositoryFactory(operations); } protected abstract T getRepository(KeyValueRepositoryFactory factory); public interface PersonRepository extends CrudRepository, KeyValueRepository { List findByAge(int age); List findByFirstname(String firstname); List findByFirstnameAndAge(String firstname, int age); Page findByAge(int age, Pageable page); List findByAgeOrFirstname(int age, String firstname); Person findByAgeAndFirstname(int age, String firstname); List findByAgeGreaterThanOrderByAgeAscFirstnameDesc(int age); List findByAgeGreaterThan(int age, Sort sort); List findByAgeGreaterThan(int age, Sort sort, Class projectionType); List findByFirstnameIn(List firstname); } interface PersonSummary { String getFirstname(); } } ================================================ FILE: src/test/java/org/springframework/data/map/CachingQuerySimpleKeyValueRepositoryUnitTests.java ================================================ /* * Copyright 2016-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import org.junit.jupiter.api.Disabled; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.query.CachingKeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.keyvalue.repository.query.SpelQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository; /** * Unit tests for {@link SimpleKeyValueRepository} using {@link CachingKeyValuePartTreeQuery} and * {@link PredicateQueryCreator}. * * @author Mark Paluch * @author Christoph Strobl */ @Disabled public class CachingQuerySimpleKeyValueRepositoryUnitTests extends SimpleKeyValueRepositoryUnitTests { @Override protected KeyValueRepositoryFactory createKeyValueRepositoryFactory(KeyValueOperations operations) { return new KeyValueRepositoryFactory(operations, PredicateQueryCreator.class, CachingKeyValuePartTreeQuery.class); } } ================================================ FILE: src/test/java/org/springframework/data/map/MapDbIntegrationTests.java ================================================ /* * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import static org.assertj.core.api.Assertions.*; import java.util.Map; import org.junit.jupiter.api.Test; import org.mapdb.DB; import org.mapdb.DBMaker; import org.mapdb.HTreeMap; import org.mapdb.Serializer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.map.repository.config.EnableMapRepositories; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Example for MapDB integration testing the repository support through {@link KeySpaceStore}. * * @author Mark Paluch */ @SpringJUnitConfig class MapDbIntegrationTests { @Configuration @EnableMapRepositories(considerNestedRepositories = true, keySpaceStoreRef = "store") static class TestConfiguration { @Bean DB db() { return DBMaker.heapDB().make(); } @Bean KeySpaceStore store(DB db) { return new KeySpaceStore() { @Override public Map getKeySpace(String keyspace) { return db.hashMap(keyspace, Serializer.JAVA, Serializer.JAVA).createOrOpen(); } @Override public void clear() { db.getStore().getAllRecids().forEachRemaining(it -> db.getStore().delete(it, Serializer.JAVA)); } }; } } @Autowired PersonRepository personRepository; @Autowired DB db; @Test void shouldStoreEntriesInMapDb() { Person walter = personRepository.save(new Person("Walter", "White")); personRepository.save(new Person("Skyler", "White")); personRepository.save(new Person("Flynn", "White")); assertThat(personRepository.countByLastname("White")).isEqualTo(3); HTreeMap backingMap = db.hashMap(Person.class.getName(), Serializer.JAVA, Serializer.JAVA) .createOrOpen(); assertThat(backingMap.size()).isEqualTo(3); assertThat(backingMap).containsEntry(walter.id, walter); } interface PersonRepository extends KeyValueRepository { long countByLastname(String lastname); } static class Person { @Id String id; String firstname; String lastname; Person(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } } } ================================================ FILE: src/test/java/org/springframework/data/map/MapKeyValueAdapterUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import static org.assertj.core.api.Assertions.*; import java.util.AbstractMap; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.util.CloseableIterator; /** * Unit tests for {@link MapKeyValueAdapter}. * * @author Christoph Strobl * @author Mark Paluch */ class MapKeyValueAdapterUnitTests { private static final String COLLECTION_1 = "collection-1"; private static final String COLLECTION_2 = "collection-2"; private static final String STRING_1 = new String("1"); private Object object1 = new SimpleObject("one"); private Object object2 = new SimpleObject("two"); private MapKeyValueAdapter adapter; @BeforeEach void setUp() { this.adapter = new MapKeyValueAdapter(LinkedHashMap.class); } @Test // DATACMNS-525 void putShouldThrowExceptionWhenAddingNullId() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.put(null, object1, COLLECTION_1)); } @Test // DATACMNS-525 void putShouldThrowExceptionWhenCollectionIsNullValue() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.put("1", object1, null)); } @Test // DATACMNS-525 void putReturnsNullWhenNoObjectForIdPresent() { assertThat(adapter.put("1", object1, COLLECTION_1)).isNull(); } @Test // DATACMNS-525 void putShouldReturnPreviousObjectForIdWhenAddingNewOneWithSameIdPresent() { adapter.put("1", object1, COLLECTION_1); assertThat(adapter.put("1", object2, COLLECTION_1)).isEqualTo(object1); } @Test // DATACMNS-525 void containsShouldThrowExceptionWhenIdIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.contains(null, COLLECTION_1)); } @Test // DATACMNS-525 void containsShouldThrowExceptionWhenTypeIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.contains("", null)); } @Test // DATACMNS-525 void containsShouldReturnFalseWhenNoElementsPresent() { assertThat(adapter.contains("1", COLLECTION_1)).isFalse(); } @Test // DATACMNS-525 void containShouldReturnTrueWhenElementWithIdPresent() { adapter.put("1", object1, COLLECTION_1); assertThat(adapter.contains("1", COLLECTION_1)).isTrue(); } @Test // DATACMNS-525 void getShouldReturnNullWhenNoElementWithIdPresent() { assertThat(adapter.get("1", COLLECTION_1)).isNull(); } @Test // DATACMNS-525 void getShouldReturnElementWhenMatchingIdPresent() { adapter.put("1", object1, COLLECTION_1); assertThat(adapter.get("1", COLLECTION_1)).isEqualTo(object1); } @Test // DATACMNS-525 void getShouldThrowExceptionWhenIdIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.get(null, COLLECTION_1)); } @Test // DATACMNS-525 void getShouldThrowExceptionWhenTypeIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.get("1", null)); } @Test // DATACMNS-525 void getAllOfShouldReturnAllValuesOfGivenCollection() { adapter.put("1", object1, COLLECTION_1); adapter.put("2", object2, COLLECTION_1); adapter.put("3", STRING_1, COLLECTION_2); assertThat((Iterable) adapter.getAllOf(COLLECTION_1)).contains(object1, object2); } @Test // DATACMNS-525 void getAllOfShouldThrowExceptionWhenTypeIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> adapter.getAllOf(null)); } @Test // DATACMNS-525 void deleteShouldReturnNullWhenGivenIdThatDoesNotExist() { assertThat(adapter.delete("1", COLLECTION_1)).isNull(); } @Test // DATACMNS-525 void deleteShouldReturnDeletedObject() { adapter.put("1", object1, COLLECTION_1); assertThat(adapter.delete("1", COLLECTION_1)).isEqualTo(object1); } @Test // DATAKV-99 void scanShouldIterateOverAvailableEntries() { adapter.put("1", object1, COLLECTION_1); adapter.put("2", object2, COLLECTION_1); CloseableIterator> iterator = adapter.entries(COLLECTION_1); assertThat(iterator.next()).isEqualTo(new AbstractMap.SimpleEntry<>("1", object1)); assertThat(iterator.next()).isEqualTo(new AbstractMap.SimpleEntry<>("2", object2)); assertThat(iterator.hasNext()).isFalse(); } @Test // DATAKV-99 void scanShouldReturnEmptyIteratorWhenNoElementsAvailable() { assertThat(adapter.entries(COLLECTION_1).hasNext()).isFalse(); } @Test // DATAKV-99 void scanDoesNotMixResultsFromMultipleKeyspaces() { adapter.put("1", object1, COLLECTION_1); adapter.put("2", object2, COLLECTION_2); CloseableIterator> iterator = adapter.entries(COLLECTION_1); assertThat(iterator.next()).isEqualTo(new AbstractMap.SimpleEntry<>("1", object1)); assertThat(iterator.hasNext()).isFalse(); } static class SimpleObject { protected String stringValue; public SimpleObject() {} SimpleObject(String value) { this.stringValue = value; } public String getStringValue() { return this.stringValue; } public void setStringValue(String stringValue) { this.stringValue = stringValue; } } } ================================================ FILE: src/test/java/org/springframework/data/map/QuerydslKeyValuePredicateExecutorUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import static org.assertj.core.api.Assertions.*; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.keyvalue.Person; import org.springframework.data.keyvalue.QPerson; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; import org.springframework.data.map.QuerydslKeyValuePredicateExecutorUnitTests.QPersonRepository; import org.springframework.data.querydsl.QSort; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.util.Streamable; /** * Unit tests for {@link org.springframework.data.keyvalue.repository.support.QuerydslKeyValuePredicateExecutor}. * * @author Christoph Strobl * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch */ class QuerydslKeyValuePredicateExecutorUnitTests extends AbstractRepositoryUnitTests { @BeforeEach void setUp() { repository.saveAll(LENNISTERS); } @Test // DATACMNS-525 void findOneIsExecutedCorrectly() { Optional result = repository.findOne(QPerson.person.firstname.eq(CERSEI.getFirstname())); assertThat(result).hasValue(CERSEI); } @Test // DATACMNS-525 void findAllIsExecutedCorrectly() { Iterable result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge())); assertThat(result).contains(CERSEI, JAIME); } @Test // DATACMNS-525 void findWithPaginationWorksCorrectly() { Page page1 = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), PageRequest.of(0, 1)); assertThat(page1.getTotalElements()).isEqualTo(2L); assertThat(page1.getContent()).hasSize(1); assertThat(page1.hasNext()).isTrue(); Page page2 = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), page1.nextPageable()); assertThat(page2.getTotalElements()).isEqualTo(2L); assertThat(page2.getContent()).hasSize(1); assertThat(page2.hasNext()).isFalse(); } @Test // DATACMNS-525 void findAllUsingOrderSpecifierWorksCorrectly() { Iterable result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), QPerson.person.firstname.desc()); assertThat(result).containsExactly(JAIME, CERSEI); } @Test // DATACMNS-525 void findAllUsingPageableWithSortWorksCorrectly() { Iterable result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), PageRequest.of(0, 10, Direction.DESC, "firstname")); assertThat(result).containsExactly(JAIME, CERSEI); } @Test // DATACMNS-525 void findAllUsingPagableWithQSortWorksCorrectly() { Iterable result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), PageRequest.of(0, 10, new QSort(QPerson.person.firstname.desc()))); assertThat(result).containsExactly(JAIME, CERSEI); } @Test // DATAKV-90 void findAllWithOrderSpecifierWorksCorrectly() { Iterable result = repository.findAll(new QSort(QPerson.person.firstname.desc())); assertThat(result).containsExactly(TYRION, JAIME, CERSEI); } @Test // DATAKV-90, DATAKV-197 void findAllShouldRequireSort() { assertThatIllegalArgumentException().isThrownBy(() -> repository.findAll((QSort) null)); } @Test // DATAKV-90, DATAKV-197 void findAllShouldAllowUnsortedFindAll() { Iterable result = repository.findAll(Sort.unsorted()); assertThat(result).contains(TYRION, JAIME, CERSEI); } @Test // DATAKV-95 void executesExistsCorrectly() { assertThat(repository.exists(QPerson.person.age.eq(CERSEI.getAge()))).isTrue(); } @Test // DATAKV-96 void shouldSupportFindAllWithPredicateAndSort() { List users = Streamable.of(repository.findAll(person.age.gt(0), Sort.by(Direction.ASC, "firstname"))) .toList(); assertThat(users).hasSize(3); assertThat(users.get(0).getFirstname()).isEqualTo(CERSEI.getFirstname()); assertThat(users.get(2).getFirstname()).isEqualTo(TYRION.getFirstname()); assertThat(users).contains(CERSEI, JAIME, TYRION); } @Test // DATAKV-179 void throwsExceptionIfMoreThanOneResultIsFound() { assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) // .isThrownBy(() -> repository.findOne(person.firstname.contains("e"))); } @Test // GH-397 void findByShouldReturnFirst() { Person first = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::firstValue); assertThat(first).isEqualTo(TYRION); first = repository.findBy(QPerson.person.firstname.eq("foo"), Function.identity()).firstValue(); assertThat(first).isNull(); } @Test // GH-397 void findByShouldReturnOne() { assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) .isThrownBy(() -> repository.findBy(QPerson.person.firstname.ne("foo"), FluentQuery.FetchableFluentQuery::one)); Person one = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::oneValue); assertThat(one).isEqualTo(TYRION); } @Test // GH-397 void findByShouldReturnFirstWithProjection() { PersonProjection interfaceProjection = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonProjection.class).firstValue()); assertThat(interfaceProjection.getFirstname()).isEqualTo("tyrion"); PersonDto dto = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonDto.class).firstValue()); assertThat(dto.getFirstname()).isEqualTo("tyrion"); } @Test // GH-397 void findByShouldReturnOneWithProjection() { PersonProjection interfaceProjection = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonProjection.class).oneValue()); assertThat(interfaceProjection.getFirstname()).isEqualTo("tyrion"); PersonDto dto = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonDto.class).oneValue()); assertThat(dto.getFirstname()).isEqualTo("tyrion"); } @Test // GH-397 void findByShouldReturnAll() { List all = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::all); assertThat(all).contains(TYRION); } @Test // GH-397 void findByShouldReturnAllSorted() { List all = repository.findBy(QPerson.person.firstname.ne("foo"), q -> q.sortBy(Sort.by(Direction.ASC, "firstname")).all()); assertThat(all).containsSequence(CERSEI, JAIME, TYRION); all = repository.findBy(QPerson.person.firstname.ne("foo"), q -> q.sortBy(Sort.by(Direction.DESC, "firstname")).all()); assertThat(all).containsSequence(TYRION, JAIME, CERSEI); } @Test // GH-397 void findByShouldReturnAllWithProjection() { Stream all = repository.findBy(QPerson.person.firstname.eq("tyrion"), q -> q.as(PersonProjection.class).stream()); assertThat(all).hasOnlyElementsOfType(PersonProjection.class); } @Test // GH-397 void findByShouldReturnPage() { Page page = repository.findBy(QPerson.person.firstname.ne("foo"), it -> it.as(PersonProjection.class).page(PageRequest.of(0, 1, Sort.by("firstname")))); assertThat(page.getContent().get(0).getFirstname()).isEqualTo("cersei"); assertThat(page.getTotalPages()).isEqualTo(3); Page nextPage = repository.findBy(QPerson.person.firstname.ne("foo"), it -> it.as(PersonProjection.class).page(page.nextPageable())); assertThat(nextPage.getContent().get(0).getFirstname()).isEqualTo("jaime"); assertThat(nextPage.getTotalPages()).isEqualTo(3); } @Test // GH-397 void findByShouldReturnStream() { List all = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::all); assertThat(all).contains(TYRION); } @Test // GH-397 void findByShouldReturnStreamWithProjection() { Stream all = repository.findBy(QPerson.person.firstname.eq("tyrion"), q -> q.as(PersonProjection.class).stream()); assertThat(all).hasOnlyElementsOfType(PersonProjection.class); } @Test // GH-397 void findByShouldReturnCount() { long count = repository.findBy(QPerson.person.firstname.ne("foo"), FluentQuery.FetchableFluentQuery::count); assertThat(count).isEqualTo(3); } @Test // GH-397 void findByShouldReturnExists() { boolean exists = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::exists); assertThat(exists).isTrue(); exists = repository.findBy(QPerson.person.firstname.eq("foo"), FluentQuery.FetchableFluentQuery::exists); assertThat(exists).isFalse(); } interface PersonProjection { String getFirstname(); } static class PersonDto { String firstname; public String getFirstname() { return this.firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } } @Override protected QPersonRepository getRepository(KeyValueRepositoryFactory factory) { return factory.getRepository(QPersonRepository.class); } interface QPersonRepository extends org.springframework.data.map.AbstractRepositoryUnitTests.PersonRepository, QuerydslPredicateExecutor {} } ================================================ FILE: src/test/java/org/springframework/data/map/SimpleKeyValueRepositoryUnitTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository; import org.springframework.data.map.AbstractRepositoryUnitTests.PersonRepository; /** * Unit tests for {@link SimpleKeyValueRepository}. * * @author Christoph Strobl * @author Oliver Gierke */ public class SimpleKeyValueRepositoryUnitTests extends AbstractRepositoryUnitTests { protected PersonRepository getRepository(KeyValueRepositoryFactory factory) { return factory.getRepository(PersonRepository.class); } } ================================================ FILE: src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.function.Predicate; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.core.PathSortAccessor; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.core.SpelQueryEngine; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.map.KeySpaceStore; import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.test.util.ReflectionTestUtils; /** * Integration tests for {@link MapRepositoryConfigurationExtension}. * * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch */ class MapRepositoriesConfigurationExtensionIntegrationTests { @Test // DATAKV-86 void registersDefaultTemplateIfReferenceNotCustomized() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class); assertThat(Arrays.asList(context.getBeanDefinitionNames())).contains("mapKeyValueTemplate"); context.close(); } @Test // DATAKV-86 void doesNotRegisterDefaultTemplateIfReferenceIsCustomized() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( ConfigWithCustomTemplateReference.class); assertThat(context.getBeanDefinitionNames()).doesNotContain("mapKeyValueTemplate"); context.close(); } @Test // GH-358 void shouldUseCustomAdapter() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( ConfigWithOverriddenTemplateReference.class); PersonRepository repository = context.getBean(PersonRepository.class); assertThatThrownBy(() -> repository.findById("foo")).hasRootCauseInstanceOf(IllegalStateException.class) .hasMessageContaining("Mock"); context.close(); } @Test // DATAKV-87 void considersMapTypeConfiguredOnAnnotation() { assertKeyValueTemplateWithAdapterFor(ConcurrentSkipListMap.class, new AnnotationConfigApplicationContext(ConfigWithCustomizedMapType.class)); } @Test // DATAKV-87 void doesNotConsiderMapConfiguredIfTemplateIsPresent() { assertKeyValueTemplateWithAdapterFor(ConcurrentHashMap.class, new AnnotationConfigApplicationContext( ConfigWithCustomizedMapTypeAndExplicitDefinitionOfKeyValueTemplate.class)); } @Test // GH-565 void considersSortAccessorConfiguredOnAnnotation() { assertKeyValueTemplateWithSortAccessorFor(PathSortAccessor.class, new AnnotationConfigApplicationContext(ConfigWithCustomizedSortAccessor.class)); } @Test // GH-576 void considersQueryEngineConfiguration() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithQueryEngine.class); KeyValueTemplate template = context.getBean(KeyValueTemplate.class); Object adapter = ReflectionTestUtils.getField(template, "adapter"); assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); Object engine = ReflectionTestUtils.getField(adapter, "engine"); assertThat(engine).isInstanceOf(SpelQueryEngine.class); } @Test // GH-576 void considersQueryEngineAndSortAccessorConfiguration() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( ConfigWithQueryEngineAndCustomizedSortAccessor.class); KeyValueTemplate template = context.getBean(KeyValueTemplate.class); Object adapter = ReflectionTestUtils.getField(template, "adapter"); assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); Object engine = ReflectionTestUtils.getField(adapter, "engine"); assertThat(engine).isInstanceOf(SpelQueryEngine.class); Object sortAccessor = ReflectionTestUtils.getField(engine, "sortAccessor"); assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL) .containsInstanceOf(PathSortAccessor.class); } private static void assertKeyValueTemplateWithAdapterFor(Class mapType, ApplicationContext context) { KeyValueTemplate template = context.getBean(KeyValueTemplate.class); Object adapter = ReflectionTestUtils.getField(template, "adapter"); assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); KeySpaceStore store = (KeySpaceStore) ReflectionTestUtils.getField(adapter, "store"); assertThat(ReflectionTestUtils.getField(store, "store")).isInstanceOf(mapType); } private static void assertKeyValueTemplateWithSortAccessorFor(Class sortAccessorType, ApplicationContext context) { KeyValueTemplate template = context.getBean(KeyValueTemplate.class); Object adapter = ReflectionTestUtils.getField(template, "adapter"); assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); Object engine = ReflectionTestUtils.getField(adapter, "engine"); Object sortAccessor = ReflectionTestUtils.getField(engine, "sortAccessor"); assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL).containsInstanceOf(sortAccessorType); } @Test // GH-576 void considersDefaultQueryCreator() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); KeyValueRepositoryFactoryBean factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class); Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator"); assertThat(queryCreator).isEqualTo(PredicateQueryCreator.class); } @Test // GH-576 void considersCustomQueryCreator() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( ConfigWithCustomQueryCreator.class); KeyValueRepositoryFactoryBean factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class); Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator"); assertThat(queryCreator).isEqualTo(MyQueryCreator.class); } @Configuration @EnableMapRepositories(considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(value = PersonRepository.class, type = FilterType.ASSIGNABLE_TYPE)) static class Config {} @Configuration @EnableMapRepositories(keyValueTemplateRef = "foo") static class ConfigWithCustomTemplateReference {} @Configuration @EnableMapRepositories(considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(value = PersonRepository.class, type = FilterType.ASSIGNABLE_TYPE)) static class ConfigWithOverriddenTemplateReference { @Bean public KeyValueOperations mapKeyValueTemplate() { return new KeyValueTemplate(keyValueAdapter()); } @Bean public KeyValueAdapter keyValueAdapter() { KeyValueAdapter mock = mock(KeyValueAdapter.class); when(mock.get(any(), anyString(), any())).thenThrow(new IllegalStateException("Mock")); return mock; } } @Configuration @EnableMapRepositories(mapType = ConcurrentSkipListMap.class) static class ConfigWithCustomizedMapType {} @Configuration @EnableMapRepositories(mapType = ConcurrentSkipListMap.class) static class ConfigWithCustomizedMapTypeAndExplicitDefinitionOfKeyValueTemplate { @Bean public KeyValueTemplate mapKeyValueTemplate() { return new KeyValueTemplate(new MapKeyValueAdapter()); } } @EnableMapRepositories(queryEngineFactory = JustSpelQueryEngineFactory.class) static class ConfigWithQueryEngine {} static class JustSpelQueryEngineFactory implements QueryEngineFactory { @Override public QueryEngine create() { return new SpelQueryEngine(); } } @EnableMapRepositories(sortAccessor = PathSortAccessor.class) static class ConfigWithCustomizedSortAccessor {} @EnableMapRepositories(sortAccessor = PathSortAccessor.class, queryEngineFactory = SpelQueryEngineFactory.class) static class ConfigWithQueryEngineAndCustomizedSortAccessor {} @EnableMapRepositories(queryCreator = MyQueryCreator.class, considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonRepository.class)) static class ConfigWithCustomQueryCreator {} static class SpelQueryEngineFactory implements QueryEngineFactory { private final SortAccessor> sortAccessor; public SpelQueryEngineFactory(SortAccessor> sortAccessor) { this.sortAccessor = sortAccessor; } @Override public QueryEngine create() { return new SpelQueryEngine(sortAccessor); } } static class MyQueryCreator extends AbstractQueryCreator>, Predicate> { public MyQueryCreator(PartTree tree) { super(tree); } public MyQueryCreator(PartTree tree, ParameterAccessor parameters) { super(tree, parameters); } @Override protected Predicate create(Part part, Iterator iterator) { return null; } @Override protected Predicate and(Part part, Predicate base, Iterator iterator) { return null; } @Override protected Predicate or(Predicate base, Predicate criteria) { return null; } @Override protected KeyValueQuery> complete(Predicate criteria, Sort sort) { return null; } } interface PersonRepository extends KeyValueRepository { } static class Person { @Id String id; String name; public String getId() { return this.id; } public String getName() { return this.name; } public void setId(String id) { this.id = id; } public void setName(String name) { this.name = name; } } } ================================================ FILE: src/test/java/org/springframework/data/map/repository/config/MapRepositoryRegistrarWithFullDefaultingIntegrationTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import static org.assertj.core.api.Assertions.*; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; import org.springframework.data.repository.CrudRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; /** * Integration tests for {@link MapRepositoriesRegistrar} with complete defaulting. * * @author Christoph Strobl * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration public class MapRepositoryRegistrarWithFullDefaultingIntegrationTests { @Configuration @EnableMapRepositories(considerNestedRepositories = true) static class Config { } @Autowired PersonRepository repo; @Test // DATAKV-86 void shouldEnableMapRepositoryCorrectly() { assertThat(repo).isNotNull(); } static class Person { @Id String id; String firstname; public String getId() { return this.id; } public String getFirstname() { return this.firstname; } public void setId(String id) { this.id = id; } public void setFirstname(String firstname) { this.firstname = firstname; } } interface PersonRepository extends CrudRepository { List findByFirstname(String firstname); } } ================================================ FILE: src/test/java/org/springframework/data/map/repository/config/MapRepositoryRegistrarWithTemplateDefinitionIntegrationTests.java ================================================ /* * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.map.repository.config; import static org.assertj.core.api.Assertions.*; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.repository.CrudRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; /** * Integration tests for {@link MapRepositoriesRegistrar} with complete defaulting. * * @author Christoph Strobl * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration public class MapRepositoryRegistrarWithTemplateDefinitionIntegrationTests { @Configuration @EnableMapRepositories(considerNestedRepositories = true) static class Config { @Bean public KeyValueOperations keyValueTemplate() { return new KeyValueTemplate(new MapKeyValueAdapter()); } } @Autowired PersonRepository repo; @Test // DATACMNS-525 void shouldEnableMapRepositoryCorrectly() { assertThat(repo).isNotNull(); } static class Person { @Id String id; String firstname; public String getId() { return this.id; } public String getFirstname() { return this.firstname; } public void setId(String id) { this.id = id; } public void setFirstname(String firstname) { this.firstname = firstname; } } interface PersonRepository extends CrudRepository { List findByFirstname(String firstname); } } ================================================ FILE: src/test/resources/logback.xml ================================================ %d %5p %40.40c:%4L - %m%n