diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 000000000..0c4b142e9 --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..ecab23c37 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,73 @@ +name: "CodeQL" + +on: + push: + branches: [ 'main', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', '2.0.x', '2.1.x', '2.2.x', '3.0.x', '3.1.x', '3.2.x', '4.0.x', '4.1.x', '4.2.x', '4.3.x', '4.4.x', '5.0.x', '5.1.x' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'main' ] + schedule: + - cron: '59 19 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - if: matrix.language == 'java' + name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index adadb46a7..78662375b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,10 @@ target/ *.iml .idea + +build/ +node_modules +node +package-lock.json + +.mvn/.develocity diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 000000000..e0857eaa2 --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,8 @@ + + + + io.spring.develocity.conventions + develocity-conventions-maven-extension + 0.0.22 + + diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 000000000..e27f6e8f5 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,14 @@ +--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 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index ef8617c5f..a54be2b1d 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -#Mon Jan 30 10:48:22 CET 2023 -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip +#Thu Jul 17 13:59:56 CEST 2025 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index 3b6378fca..d1245b878 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ def p = [:] node { - checkout scm - p = readProperties interpolate: true, file: 'ci/pipeline.properties' + checkout scm + p = readProperties interpolate: true, file: 'ci/pipeline.properties' } pipeline { @@ -18,7 +18,7 @@ pipeline { } stages { - stage("test: baseline (Java 17)") { + stage("test: baseline (main)") { when { beforeAgent(true) anyOf { @@ -31,16 +31,47 @@ pipeline { } options { timeout(time: 30, unit: 'MINUTES') } environment { - DOCKER_HUB = credentials("${p['docker.credentials']}") ARTIFACTORY = credentials("${p['artifactory.credentials']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.withRegistry(p['docker.registry'], p['docker.credentials']) { + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) { - sh "docker login --username ${DOCKER_HUB_USR} --password ${DOCKER_HUB_PSW}" - sh 'PROFILE=ci ci/test.sh' - sh "ci/clean.sh" + sh "PROFILE=ci JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + } + } + } + } + } + + stage("Test other configurations") { + when { + beforeAgent(true) + allOf { + branch(pattern: "main|(\\d\\.\\d\\.x)", comparator: "REGEXP") + not { triggeredBy 'UpstreamCause' } + } + } + parallel { + stage("test: baseline (next)") { + agent { + label 'data' + } + options { timeout(time: 30, unit: 'MINUTES') } + environment { + ARTIFACTORY = credentials("${p['artifactory.credentials']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") + } + steps { + script { + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) { + sh "PROFILE=ci JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + } + } } } } @@ -59,23 +90,25 @@ pipeline { label 'data' } options { timeout(time: 20, unit: 'MINUTES') } - environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } - steps { script { - docker.withRegistry(p['docker.registry'], p['docker.credentials']) { - docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { - sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase-non-root ' + - '-Dartifactory.server=https://repo.spring.io ' + + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) { + sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + + "./mvnw -s settings.xml -Pci,artifactory " + + "-Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root " + + "-Dartifactory.server=${p['artifactory.url']} " + "-Dartifactory.username=${ARTIFACTORY_USR} " + "-Dartifactory.password=${ARTIFACTORY_PSW} " + - "-Dartifactory.staging-repository=libs-snapshot-local " + + "-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " + "-Dartifactory.build-name=spring-data-couchbase " + - "-Dartifactory.build-number=${BUILD_NUMBER} " + - '-Dmaven.test.skip=true clean deploy -U -B' + "-Dartifactory.build-number=spring-data-couchbase-${BRANCH_NAME}-build-${BUILD_NUMBER} " + + "-Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase " + + "-Dmaven.test.skip=true clean deploy -U -B" } } } @@ -86,10 +119,6 @@ pipeline { post { changed { script { - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#spring-data-dev', - message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") emailext( subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", mimeType: 'text/html', diff --git a/README.adoc b/README.adoc index e6a3f4812..14d6c6e26 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,4 @@ -image:https://spring.io/badges/spring-data-couchbase/ga.svg[Spring Data Couchbase,link=https://projects.spring.io/spring-data-couchbase#quick-start] image:https://spring.io/badges/spring-data-couchbase/snapshot.svg[Spring Data Couchbase,link=https://projects.spring.io/spring-data-couchbase#quick-start] - -= Spring Data Couchbase image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-couchbase%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-couchbase/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]] += Spring Data Couchbase image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-couchbase%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-couchbase/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]] 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 Couchbase"] The primary goal of the https://www.springsource.org/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 @@ -32,16 +30,15 @@ This project is lead and maintained by Couchbase, Inc. == Version compatibility -`Spring-Data Couchbase 3.0.x` is the Spring Data connector for the `Couchbase Java SDK 2.x` generation. +`Spring-Data Couchbase` is the Spring Data connector for the `Couchbase Java SDK 2.x` generation. -Both the SDK and this Spring Data community project are major version changes with lots of differences from their -respective previous versions. +Both the SDK and this Spring Data community project are major version changes with lots of differences from their respective previous versions. Notably, this version is compatible with `Couchbase Server 4.0`, bringing support for the `N1QL` query language. == Code of Conduct -This project is governed by the https://github.com/spring-projects/.github/blob/e3cc2ff230d8f1dca06535aa6b5a4a23815861d4/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. +This project is governed by the https://github.com/spring-projects/.github/blob/e3cc2ff230d8f1dca06535aa6b5a4a23815861d4/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 @@ -124,9 +121,9 @@ If you'd rather like the latest snapshots of the upcoming major version, use our - spring-libs-snapshot + spring-snapshot Spring Snapshot Repository - https://repo.spring.io/libs-snapshot + https://repo.spring.io/snapshot ---- @@ -144,14 +141,16 @@ You can also chat with the community on https://gitter.im/spring-projects/spring == Reporting Issues -Spring Data uses JIRA as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below: +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://jira.spring.io/browse/DATACOUCH[issue tracker] to see if someone has already reported the problem. -* If the issue doesn’t already exist, https://jira.spring.io/browse/DATACOUCH[create a new issue]. +https://github.com/spring-projects/spring-data-couchbase/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-couchbase/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 and JVM version. -* If you need to paste code, or include a stack trace use JIRA `{code}…{code}` escapes before and after your text. -* 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. +* If you need to paste code, or include a stack trace use Markdown +++```+++ escapes before and after your text. +* 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 @@ -173,10 +172,10 @@ Building the documentation builds also the project without running tests. [source,bash] ---- - $ ./mvnw clean install -Pdistribute + $ ./mvnw clean install -Pantora ---- -The generated documentation is available from `target/site/reference/html/index.html`. +The generated documentation is available from `target/antora/site/index.html`. === Building and staging reference documentation for review @@ -185,7 +184,7 @@ The generated documentation is available from `target/site/reference/html/index. export MY_GIT_USER= mvn generate-resources docs=`pwd`/target/site/reference/html - pushd /tmp + pushd /tmp mkdir $$ cd $$ # see https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site diff --git a/ci/clean.sh b/ci/clean.sh index 4986aaf40..732797ea2 100755 --- a/ci/clean.sh +++ b/ci/clean.sh @@ -2,5 +2,7 @@ set -euo pipefail -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \ - ./mvnw -s settings.xml clean -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase +export JENKINS_USER=${JENKINS_USER_NAME} + +MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \ + ./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 5b5444523..a79feac95 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,19 +1,20 @@ # Java versions -java.main.tag=17.0.6_10-jdk-focal +java.main.tag=24.0.1_9-jdk-noble +java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard -docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.main.tag} +docker.java.main.image=library/eclipse-temurin:${java.main.tag} +docker.java.next.image=library/eclipse-temurin:${java.next.tag} # Supported versions of MongoDB -docker.mongodb.4.4.version=4.4.18 -docker.mongodb.5.0.version=5.0.14 -docker.mongodb.6.0.version=6.0.4 +docker.mongodb.6.0.version=6.0.23 +docker.mongodb.7.0.version=7.0.20 +docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis -docker.redis.6.version=6.2.10 - -# Supported versions of Cassandra -docker.cassandra.3.version=3.11.14 +docker.redis.6.version=6.2.13 +docker.redis.7.version=7.2.4 +docker.valkey.8.version=8.1.1 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home @@ -22,4 +23,10 @@ docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock - # Credentials docker.registry= docker.credentials=hub.docker.com-springbuildmaster +docker.proxy.registry=https://docker-hub.usw1.packages.broadcom.com +docker.proxy.credentials=usw1_packages_broadcom_com-jenkins-token artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c +artifactory.url=https://repo.spring.io +artifactory.repository.snapshot=libs-snapshot-local +develocity.access-key=gradle_enterprise_secret_access_key +jenkins.user.name=spring-builds+jenkins diff --git a/ci/test.sh b/ci/test.sh index 5601c3736..0d015bed4 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -3,8 +3,9 @@ set -euo pipefail mkdir -p /tmp/jenkins-home/.m2/spring-data-couchbase -chown -R 1001:1001 . -MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \ +export JENKINS_USER=${JENKINS_USER_NAME} + +MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ - -P${PROFILE} clean dependency:list test -Dsort -Dbundlor.enabled=false -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase + -P${PROFILE} clean dependency:list test -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/package.json b/package.json new file mode 100644 index 000000000..057a40fe8 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "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" + } +} diff --git a/pom.xml b/pom.xml index 8f65493ac..7d6018f09 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 org.springframework.data spring-data-couchbase - 5.1.0-M2 + 6.0.0-SNAPSHOT Spring Data Couchbase Spring Data integration for Couchbase @@ -14,22 +14,19 @@ org.springframework.data.build spring-data-parent - 3.1.0-M2 + 4.0.0-SNAPSHOT - 3.4.1 - 3.4.1 - 3.1.0-M2 + 3.8.2 + 4.0.0-SNAPSHOT spring.data.couchbase 7.0.1.Final - 1.1.3 - 5.0.0 - 3.7.4 3.1.0 2.10.13 2.13.4 4.0.0 + 6.11 @@ -47,9 +44,9 @@ - com.querydsl + io.github.openfeign.querydsl querydsl-apt - ${querydsl} + ${querydsl_of} provided @@ -247,32 +244,32 @@ - - - spring-libs-milestone - https://repo.spring.io/libs-milestone - - - sonatype-snapshot - https://oss.sonatype.org/content/repositories/snapshots - - true - - - false - - - - - - - spring-plugins-release - https://repo.spring.io/plugins-release - - - + + org.apache.maven.plugins + maven-compiler-plugin + + + test-annotation-processing + generate-test-sources + + testCompile + + + only + + org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor + + target/generated-test-sources + + -Aquerydsl.logInfo=true + + + + + + org.apache.maven.plugins maven-surefire-plugin @@ -313,33 +310,54 @@ maven-assembly-plugin - org.asciidoctor - asciidoctor-maven-plugin - - - com.mysema.maven - apt-maven-plugin - ${apt} - - - com.querydsl - querydsl-apt - ${querydsl} - - - - - generate-test-sources - - test-process - - - target/generated-test-sources - org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor - - - + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + + 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 + + diff --git a/src/main/antora/.github/workflows/deploy-docs.yml b/src/main/antora/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..9d272779a --- /dev/null +++ b/src/main/antora/.github/workflows/deploy-docs.yml @@ -0,0 +1,33 @@ +name: Deploy Docs +on: + push: + branches-ignore: [ gh-pages ] + tags: '**' + repository_dispatch: + types: request-build-reference # legacy + #schedule: + #- cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: +permissions: + actions: write +jobs: + build: + runs-on: ubuntu-latest + # FIXME: enable when pushed to spring-projects + # if: github.repository_owner == 'spring-projects' + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: docs-build + fetch-depth: 1 + - name: Dispatch (partial build) + if: github.ref_type == 'branch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} + - name: Dispatch (full build) + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml new file mode 100644 index 000000000..af730e043 --- /dev/null +++ b/src/main/antora/antora-playbook.yml @@ -0,0 +1,40 @@ +# 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-couchbase' +site: + title: Spring Data Couchbase + url: https://docs.spring.io/spring-data-couchbase/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, 3.2.x ] + 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 diff --git a/src/main/antora/antora.yml b/src/main/antora/antora.yml new file mode 100644 index 000000000..7c0f37f68 --- /dev/null +++ b/src/main/antora/antora.yml @@ -0,0 +1,17 @@ +name: data-couchbase +version: true +title: Spring Data Couchbase +nav: + - modules/ROOT/nav.adoc +ext: + collector: + - run: + command: ./mvnw validate process-resources -am -Pantora-process-resources + local: true + scan: + dir: target/classes/ + - run: + command: ./mvnw package -Pdistribute + local: true + scan: + dir: target/antora diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc new file mode 100644 index 000000000..ac8b6a553 --- /dev/null +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -0,0 +1,32 @@ +* xref:index.adoc[Overview] +** xref:commons/upgrade.adoc[] +** xref:commons/migrating.adoc[] + +* xref:couchbase.adoc[] +** xref:couchbase/configuration.adoc[] +** xref:couchbase/entity.adoc[] +** xref:couchbase/autokeygeneration.adoc[] +** xref:couchbase/template.adoc[] +** xref:couchbase/transactions.adoc[] +** xref:couchbase/collections.adoc[] +** xref:couchbase/fieldlevelencryption.adoc[] +** xref:couchbase/ansijoins.adoc[] +** xref:couchbase/caching.adoc[] + +* xref:repositories.adoc[] +** xref:repositories/core-concepts.adoc[] +** xref:repositories/definition.adoc[] +** xref:couchbase/repository.adoc[] +** xref:couchbase/reactiverepository.adoc[] +** xref:repositories/create-instances.adoc[] +** xref:repositories/query-methods-details.adoc[] +** xref:repositories/projections.adoc[] +** xref:repositories/custom-implementations.adoc[] +** xref:repositories/core-domain-events.adoc[] +** xref:repositories/core-extensions.adoc[] +** xref:repositories/null-handling.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] diff --git a/src/main/asciidoc/migrating.adoc b/src/main/antora/modules/ROOT/pages/commons/migrating.adoc similarity index 72% rename from src/main/asciidoc/migrating.adoc rename to src/main/antora/modules/ROOT/pages/commons/migrating.adoc index 1c399611e..e2406ee3a 100644 --- a/src/main/asciidoc/migrating.adoc +++ b/src/main/antora/modules/ROOT/pages/commons/migrating.adoc @@ -12,45 +12,48 @@ Since the main objective was to migrate from the Java SDK 2 to 3, configuration IMPORTANT: XML Configuration support has been dropped, so only java/annotation based configuration is supported. -Your configuration still has to extend the `AbstractCouchbaseConfiguration`, but since RBAC (role-based access control) is now mandatory, different properties need to be overridden in order to be configured: `getConnectionString`, `getUserName`, `getPassword` and `getBucketName`. If you want to use a non-default scope optionally you can override the `getScopeName` method. Note that if you want to use certificate based authentication or you need to customize the password authentication, the `authenticator` method can be overridden to perform this task. +Your configuration still has to extend the `AbstractCouchbaseConfiguration`, but since RBAC (role-based access control) is now mandatory, different properties need to be overridden in order to be configured: `getConnectionString`, `getUserName`, `getPassword` and `getBucketName`.If you want to use a non-default scope optionally you can override the `getScopeName` method.Note that if you want to use certificate based authentication or you need to customize the password authentication, the `authenticator` method can be overridden to perform this task. The new SDK still has an environment that is used to configure it, so you can override the `configureEnvironment` method and supply custom configuration if needed. -For more information, see <>. +For more information, see xref:couchbase/configuration.adoc[Installation & Configuration]. +[[spring-boot-version-compatibility]] === Spring Boot Version Compatibility -Spring Boot 2.3.x or higher depends on Spring Data Couchbase 4.x. Earlier versions of Couchbase are not available because SDK 2 and 3 cannot live on the same classpath. +Spring Boot 2.3.x or higher depends on Spring Data Couchbase 4.x.Earlier versions of Couchbase are not available because SDK 2 and 3 cannot live on the same classpath. [[couchbase.migrating.entities]] +[[entities]] == Entities How to deal with entities has not changed, although since the SDK now does not ship annotations anymore only Spring-Data related annotations are supported. Specifically: - - `com.couchbase.client.java.repository.annotation.Id` became `import org.springframework.data.annotation.Id` - - `com.couchbase.client.java.repository.annotation.Field` became `import org.springframework.data.couchbase.core.mapping.Field` +- `com.couchbase.client.java.repository.annotation.Id` became `import org.springframework.data.annotation.Id` +- `com.couchbase.client.java.repository.annotation.Field` became `import org.springframework.data.couchbase.core.mapping.Field` The `org.springframework.data.couchbase.core.mapping.Document` annotation stayed the same. -For more information, see <>. +For more information, see xref:couchbase/entity.adoc[Modeling Entities]. [[couchbase.migrating.indexes]] == Automatic Index Management -Automatic Index Management has been redesigned to allow more flexible indexing. New annotations have been introduced and old ones like `@ViewIndexed`, `@N1qlSecondaryIndexed` and `@N1qlPrimaryIndexed` were removed. +Automatic Index Management has been redesigned to allow more flexible indexing. +New annotations have been introduced and old ones like `@ViewIndexed`, `@N1qlSecondaryIndexed` and `@N1qlPrimaryIndexed` were removed. -For more information, see <>. +For more information, see xref:couchbase/repository.adoc#couchbase.repository.indexing[Automatic Index Management]. [[couchbase.migrating.template]] == Template and ReactiveTemplate Since the Couchbase SDK 3 removes support for `RxJava` and instead adds support for `Reactor`, both the `couchbaseTemplate` as well as the `reactiveCouchbaseTemplate` can be directly accessed from the `AbstractCouchbaseConfiguration`. -The template has been completely overhauled so that it now uses a fluent API to configure instead of many method overloads. This has the advantage that in the future we are able to extend the functionality without having to introduce more and more overloads that make it complicated to navigate. +The template has been completely overhauled so that it now uses a fluent API to configure instead of many method overloads.This has the advantage that in the future we are able to extend the functionality without having to introduce more and more overloads that make it complicated to navigate. The following table describes the method names in 3.x and compares them to their 4.x equivalents: @@ -113,14 +116,14 @@ In addition, the following methods have been added which were not available in 3 We tried to unify and align the APIs more closely to the underlying SDK semantics so they are easier to correlate and navigate. -For more information, see <>. +For more information, see xref:couchbase/template.adoc[Template & direct operations]. [[couchbase.migrating.repository]] == Repositories & Queries - - `org.springframework.data.couchbase.core.query.Query` became `org.springframework.data.couchbase.repository.Query` - - `org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository` has been removed. Consider extending `ReactiveSortingRepository` or `ReactiveCouchbaseRepository` - - `org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository` has been removed. Consider extending `PagingAndSortingRepository` or `CouchbaseRepository` +- `org.springframework.data.couchbase.core.query.Query` became `org.springframework.data.couchbase.repository.Query` +- `org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository` has been removed.Consider extending `ReactiveSortingRepository` or `ReactiveCouchbaseRepository` +- `org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository` has been removed.Consider extending `PagingAndSortingRepository` or `CouchbaseRepository` IMPORTANT: Support for views has been removed and N1QL queries are now the first-class citizens for all custom repository methods as well as the built-in ones by default. @@ -151,9 +154,10 @@ public class MyService { ---- ==== -See <> for more information. +See xref:couchbase/repository.adoc[Couchbase repositories] for more information. +[[full-text-search-fts]] == Full Text Search (FTS) The FTS API has been simplified and now can be accessed via the `Cluster` class: @@ -193,4 +197,4 @@ public class MyService { ---- ==== -See link:https://docs.couchbase.com/java-sdk/current/howtos/full-text-searching-with-sdk.html[the FTS Documentation] for more information. \ No newline at end of file +See link:https://docs.couchbase.com/java-sdk/current/howtos/full-text-searching-with-sdk.html[the FTS Documentation] for more information. diff --git a/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc b/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc new file mode 100644 index 000000000..51a9189aa --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$upgrade.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/couchbase.adoc b/src/main/antora/modules/ROOT/pages/couchbase.adoc new file mode 100644 index 000000000..0b1c813e1 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/couchbase.adoc @@ -0,0 +1,15 @@ +[[couchbase.core]] += Couchbase Support +:page-section-summary-toc: 1 + +Spring Data support for Couchbase contains a wide range of features: + +* Spring configuration support with xref:couchbase/configuration.adoc[Java-based `@Configuration` classes]. +* The xref:couchbase/template.adoc[`CouchbaseTemplate` and `ReactiveCouchbaseTemplate`] helper classes that provide object mapping between Couchbase collections and POJOs. +* xref:couchbase/template.adoc#exception-translation[Exception translation] into Spring's portable {spring-data-commons-docs-url}data-access.html#dao-exceptions[Data Access Exception Hierarchy]. +* Feature rich object mapping integrated with _Spring's_ {spring-data-commons-docs-url}core.html#core-convert[Conversion Service]. +* Annotation-based mapping metadata that is extensible to support other metadata formats. +* Automatic implementation of xref:repositories.adoc[imperative and reactive `Repository` interfaces] including support for xref:repositories/custom-implementations.adoc[custom query methods]. + +For most data-oriented tasks, you can use the `[Reactive]CouchbaseTemplate` or the `Repository` support, both of which use the rich object-mapping functionality. +Spring Data Couchbase uses consistent naming conventions on objects in various APIs to those found in the Couchbase Java SDK so that they are familiar and so that you can map your existing knowledge onto the Spring APIs. diff --git a/src/main/asciidoc/ansijoins.adoc b/src/main/antora/modules/ROOT/pages/couchbase/ansijoins.adoc similarity index 95% rename from src/main/asciidoc/ansijoins.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/ansijoins.adoc index 732f39ae1..11c756948 100644 --- a/src/main/asciidoc/ansijoins.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/ansijoins.adoc @@ -58,16 +58,19 @@ List books; [[couchbase.ansijoins.joinhints]] == ANSI Join Hints +[[use-index-hint]] === Use Index Hint `index` element on the `@N1qlJoin` can be used to provided the hint for the `lks` (current entity) index and `rightIndex` element can be used to provided the `rks` (associated entity) index. +[[hash-join-hint]] === Hash Join Hint If the join type is going to be hash join, the hash side can be specified for the `rks` (associated entity). If the associated entity is on the build side, it can be specified as `HashSide.BUILD` else `HashSide.PROBE`. +[[use-keys-hint]] === Use Keys Hint -`keys` element on the `@N1qlJoin` annotation can be used to specify unique document keys to restrict the join key space. \ No newline at end of file +`keys` element on the `@N1qlJoin` annotation can be used to specify unique document keys to restrict the join key space. diff --git a/src/main/asciidoc/autokeygeneration.adoc b/src/main/antora/modules/ROOT/pages/couchbase/autokeygeneration.adoc similarity index 90% rename from src/main/asciidoc/autokeygeneration.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/autokeygeneration.adoc index 8ddf7cce0..03cc71f01 100644 --- a/src/main/asciidoc/autokeygeneration.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/autokeygeneration.adoc @@ -4,8 +4,8 @@ This chapter describes how couchbase document keys can be auto-generated using builtin mechanisms. There are two types of auto-generation strategies supported. -- <> -- <> +- xref:couchbase/autokeygeneration.adoc#couchbase.autokeygeneration.usingattributes[Key generation using attributes] +- xref:couchbase/autokeygeneration.adoc#couchbase.autokeygeneration.unique[Key generation using uuid] NOTE: The maximum key length supported by couchbase is 250 bytes. @@ -75,4 +75,4 @@ public class User { ... } ---- -==== \ No newline at end of file +==== diff --git a/src/main/asciidoc/caching.adoc b/src/main/antora/modules/ROOT/pages/couchbase/caching.adoc similarity index 97% rename from src/main/asciidoc/caching.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/caching.adoc index 5aacff6a5..cffc00b59 100644 --- a/src/main/asciidoc/caching.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/caching.adoc @@ -54,4 +54,4 @@ public String simulateLongRun(long time) { If you run the method multiple times, you'll see a set operation happening first, followed by multiple get operations and no sleep time (which fakes the expensive execution). You can store whatever you want, if it is JSON of course you can access it through views and look at it in the Web UI. -Note that to use cache.clear() or catch.invalidate(), the bucket must have a primary key. +Note that to use cache.clear() or cache.invalidate(), the bucket must have a primary key. diff --git a/src/main/asciidoc/collections.adoc b/src/main/antora/modules/ROOT/pages/couchbase/collections.adoc similarity index 97% rename from src/main/asciidoc/collections.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/collections.adoc index d6fbf18d0..fc49b4db6 100644 --- a/src/main/asciidoc/collections.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/collections.adoc @@ -7,14 +7,17 @@ The https://github.com/couchbaselabs/try-cb-spring[try-cb-spring] sample applica The 2021 Couchbase Connect presentation on Collections in Spring Data can be found at https://www.youtube.com/watch?v=MrplTeEFItk[Presentation Only] and https://web.cvent.com/hub/events/1dce8283-986d-4de9-8368-94c98f60df01/sessions/9ee89a85-833c-4e0c-81b0-807864fa351b?goBackHref=%2Fevents%2F1dce8283-986d-4de9-8368-94c98f60df01%2Fsessions&goBackName=Add%2FView+Sessions&goBackTab=all[Presentation with Slide Deck] +[[requirements]] == Requirements - Couchbase Server 7.0 or above. - Spring Data Couchbase 4.3.1 or above. +[[getting-started-configuration]] == Getting Started & Configuration +[[scope-and-collection-specification]] === Scope and Collection Specification There are several mechanisms of specifying scopes and collections, and these may be combined, or one mechanism may override another. First some definitions for scopes and collections. An unspecified scope indicates that the default scope is to be used, likewise, an diff --git a/src/main/antora/modules/ROOT/pages/couchbase/configuration.adoc b/src/main/antora/modules/ROOT/pages/couchbase/configuration.adoc new file mode 100644 index 000000000..491861d45 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/couchbase/configuration.adoc @@ -0,0 +1,252 @@ +[[couchbase.configuration]] += Installation & Configuration + +This chapter describes the common installation and configuration steps needed when working with the library. + +[[installation]] +== Installation + +All versions intended for production use are distributed across Maven Central and the Spring release repository. +As a result, the library can be included like any other maven dependency: + + +[[compatibility]] +== Compatibility + +The simplest way to get the correct dependencies is by making a project with https://start.spring.io/[spring initializr] +The parent Spring Boot Starter artfacts have the required dependencies, they do not need to be specified. + +=== Spring Boot Version Compatibility + +* Spring Boot 3.4.* uses Spring Data Couchbase 5.4.*. +* Spring Boot 3.3.* uses Spring Data Couchbase 5.3.*. +* Spring Boot 3.2.* uses Spring Data Couchbase 5.2.*. + +=== Couchbase Java SDK Compatibility + +* Spring Data Couchbase 5.4.* depends on Couchbase Java SDK 3.7.* +* Spring Data Couchbase 5.3.* depends on Couchbase Java SDK 3.6.* +* Spring Data Couchbase 5.2.* depends on Couchbase Java SDK 3.3.* + +[[configuration]] +== Configuration +.Including the dependency through maven +==== +[source,xml,subs="+attributes"] +---- + + org.springframework.data + spring-data-couchbase + {version} + +---- +==== + +This will pull in several dependencies, including the underlying Couchbase Java SDK, common Spring dependencies and also Jackson as the JSON mapping infrastructure. + +You can also grab snapshots from the https://repo.spring.io/ui/repos/tree/General/snapshot/org/springframework/data/spring-data-couchbase[spring snapshot repository] ( \https://repo.spring.io/snapshot ) and milestone releases from the https://repo.spring.io/ui/repos/tree/General/milestone/org/springframework/data/spring-data-couchbase[spring milestone repository] ( \https://repo.spring.io/milestone ). +Here is an example on how to use the current SNAPSHOT dependency: + +[[snapshot-configuration]] +== Snapshot Configuration + +.Using a snapshot version +==== +[source,xml] +---- + + org.springframework.data + spring-data-couchbase + ${version}-SNAPSHOT + + + + spring-snapshot + Spring Snapshot Repository + https://repo.spring.io/snapshot + +---- +==== + +[[overriding-the-couchbase-sdk-version]] +== Overriding the Couchbase SDK Version + +Some users may wish to use a Couchbase Java SDK version different from the one referenced in a Spring Data Couchbase release for the purpose of obtaining bug and vulnerability fixes. Since Couchbase Java SDK minor version releases are backwards compatible, this version of Spring Data Couchbase is compatible and supported with any 3.x version of the Couchbase Java SDK newer than the one specified in the release dependencies. To change the Couchbase Java SDK version used by Spring Data Couchbase, simply override the dependency in the application pom.xml as follows: + +.If Using the spring-data-couchbase Dependency Directly +==== +[source,xml] +---- + + org.springframework.data + spring-data-couchbase + ${version} + + + com.couchbase.client + java-client + + + + + + com.couchbase.client + java-client + 3.4.7 + +---- +==== + +.If Using the spring-data-starter-couchbase Dependency (from Spring Initialzr) +==== +[source,xml] +---- + + org.springframework.boot + spring-boot-starter-parent + x.y.z + + + + + org.springframework.boot + spring-boot-starter-data-couchbase + + + com.couchbase.client + java-client + + + + + + com.couchbase.client + java-client + 3.4.7 + +---- +==== + +Once you have all needed dependencies on the classpath, you can start configuring it. +Only Java config is supported (XML config has been removed in 4.0). + +[[configuration-java]] +== Annotation-based Configuration ("JavaConfig") + +To get started, all you need to do is subclass the `AbstractCouchbaseConfiguration` and implement the abstract methods. + +.Extending the `AbstractCouchbaseConfiguration` +==== +[source,java] +---- + +@Configuration +public class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return "couchbase://127.0.0.1"; + } + + @Override + public String getUserName() { + return "Administrator"; + } + + @Override + public String getPassword() { + return "password"; + } + + @Override + public String getBucketName() { + return "travel-sample"; + } +} +---- +==== + +The connection string is made up of a list of hosts and an optional scheme (`couchbase://`) as shown in the code above. +All you need to provide is a list of Couchbase nodes to bootstrap into (separated by a `,`). Please note that while one +host is sufficient in development, it is recommended to add 3 to 5 bootstrap nodes here. Couchbase will pick up all nodes +from the cluster automatically, but it could be the case that the only node you've provided is experiencing issues while +you are starting the application. + +The `userName` and `password` are configured in your Couchbase Server cluster through RBAC (role-based access control). +The `bucketName` reflects the bucket you want to use for this configuration. + +Additionally, the SDK environment can be tuned by overriding the `configureEnvironment` method which takes a +`ClusterEnvironment.Builder` to return a configured `ClusterEnvironment`. + +Many more things can be customized and overridden as custom beans from this configuration (for example repositories, +validation and custom converters). + +TIP: If you use `SyncGateway` and `CouchbaseMobile`, you may run into problem with fields prefixed by `_`. +Since Spring Data Couchbase by default stores the type information as a `_class` attribute this can be problematic. +Override `typeKey()` (for example to return `MappingCouchbaseConverter.TYPEKEY_SYNCGATEWAY_COMPATIBLE`) to change the +name of said attribute. + +If you start your application, you should see Couchbase INFO level logging in the logs, indicating that the underlying +Couchbase Java SDK is connecting to the database.If any errors are reported, make sure that the given credentials +and host information are correct. + + +[[configuring-multiple-buckets]] +== Configuring Multiple Buckets + +To leverage multi-bucket repositories, implement the methods below in your Config class. +The config*OperationsMapping methods configure the mapping of entity-objects to buckets. +Be careful with the method names - using a method name that is a Bean will result in the value of that bean being used instead of the result of the method. + +This example maps Person -> protected, User -> mybucket, and everything else goes to getBucketName(). +Note that this only maps calls through the Repository. + +==== +[source,java] +---- +@Override +public void configureReactiveRepositoryOperationsMapping(ReactiveRepositoryOperationsMapping baseMapping) { + try { + ReactiveCouchbaseTemplate personTemplate = myReactiveCouchbaseTemplate(myCouchbaseClientFactory("protected"),new MappingCouchbaseConverter()); + baseMapping.mapEntity(Person.class, personTemplate); // Person goes in "protected" bucket + ReactiveCouchbaseTemplate userTemplate = myReactiveCouchbaseTemplate(myCouchbaseClientFactory("mybucket"),new MappingCouchbaseConverter()); + baseMapping.mapEntity(User.class, userTemplate); // User goes in "mybucket" + // everything else goes in getBucketName() + } catch (Exception e) { + throw e; + } +} +@Override +public void configureRepositoryOperationsMapping(RepositoryOperationsMapping baseMapping) { + try { + CouchbaseTemplate personTemplate = myCouchbaseTemplate(myCouchbaseClientFactory("protected"),new MappingCouchbaseConverter()); + baseMapping.mapEntity(Person.class, personTemplate); // Person goes in "protected" bucket + CouchbaseTemplate userTemplate = myCouchbaseTemplate(myCouchbaseClientFactory("mybucket"),new MappingCouchbaseConverter()); + baseMapping.mapEntity(User.class, userTemplate); // User goes in "mybucket" + // everything else goes in getBucketName() + } catch (Exception e) { + throw e; + } +} + +// do not use reactiveCouchbaseTemplate for the name of this method, otherwise the value of that bean +// will be used instead of the result of this call (the client factory arg is different) +public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter); +} + +// do not use couchbaseTemplate for the name of this method, otherwise the value of that been +// will be used instead of the result from this call (the client factory arg is different) +public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter); +} + +// do not use couchbaseClientFactory for the name of this method, otherwise the value of that bean will +// will be used instead of this call being made ( bucketname is an arg here, instead of using bucketName() ) +public CouchbaseClientFactory myCouchbaseClientFactory(String bucketName) { + return new SimpleCouchbaseClientFactory(getConnectionString(),authenticator(), bucketName ); +} +---- +==== diff --git a/src/main/asciidoc/entity.adoc b/src/main/antora/modules/ROOT/pages/couchbase/entity.adoc similarity index 98% rename from src/main/asciidoc/entity.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/entity.adoc index ed9c3dceb..c4b1dc749 100644 --- a/src/main/asciidoc/entity.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/entity.adoc @@ -3,7 +3,7 @@ This chapter describes how to model Entities and explains their counterpart representation in Couchbase Server itself. -include::{spring-data-commons-docs}/object-mapping.adoc[leveloffset=+1] +include::{commons}@data-commons::page$object-mapping.adoc[leveloffset=+1] [[basics]] == Documents and Fields @@ -78,8 +78,8 @@ This key needs to be any string with a length of maximum 250 characters. Feel free to use whatever fits your use case, be it a UUID, an email address or anything else. Writes to Couchbase-Server buckets can optionally be assigned durability requirements; which instruct Couchbase Server to update the specified document on multiple nodes in memory and/or disk locations across the cluster; before considering the write to be committed. -Default durability requirements can also be configured through the `@Document` annotation. -For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes. +Default durability requirements can also be configured through the `@Document` or `@Durability` annotations. +For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes. Both of the annotations support expression based durability level assignment via `durabilityExpression` attribute (Note SPEL is not supported). [[datatypes]] == Datatypes and Converters diff --git a/src/main/asciidoc/fieldlevelencryption.adoc b/src/main/antora/modules/ROOT/pages/couchbase/fieldlevelencryption.adoc similarity index 95% rename from src/main/asciidoc/fieldlevelencryption.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/fieldlevelencryption.adoc index 7554180ab..e8a8de5a3 100644 --- a/src/main/asciidoc/fieldlevelencryption.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/fieldlevelencryption.adoc @@ -3,16 +3,20 @@ Couchbase supports https://docs.couchbase.com/java-sdk/current/howtos/encrypting-using-sdk.html[Field Level Encryption]. This section documents how to use it with Spring Data Couchbase. +[[requirements]] == Requirements - Spring Data Couchbase 5.0.0-RC1 or above. +[[overview]] == Overview Fields annotated with com.couchbase.client.java.encryption.annotation.Encrypted (@Encrypted) will be automatically encrypted on write and decrypted on read. Unencrypted fields can be migrated to encrypted by specifying @Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED). +[[getting-started-configuration]] == Getting Started & Configuration +[[dependencies]] === Dependencies Field Level Encryption is available with the dependency ( see https://docs.couchbase.com/java-sdk/current/howtos/encrypting-using-sdk.html[Field Level Encryption] ) @@ -25,6 +29,7 @@ HashiCorp Vault Transit integration requires https://docs.spring.io/spring-vault org.springframework.vault spring-vault-core ``` +[[providing-a-cryptomanager]] === Providing a CryptoManager A CryptoManager needs to be provided by overriding the cryptoManager() method in AbstractCouchbaseConfiguration. This CryptoManager will be used by Spring Data Couchbase and also by Couchbase Java SDK direct calls made from a CouchbaseClientFactory. @@ -43,15 +48,18 @@ protected CryptoManager cryptoManager() { CryptoManager cryptoManager = DefaultCryptoManager.builder().decrypter(provider.decrypter()) .defaultEncrypter(provider.encrypterForKey("myKey")).build(); + return cryptoManager; } ``` +[[defining-a-field-as-encrypted-]] === Defining a Field as Encrypted. 1. @Encrypted defines a field as encrypted. 2. @Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED) defines a field that may or may not be encrypted when read. It will be encrypted when written. -3. @Encrypted(encrypter = "") specifies the alias of the encrypter to use for encryption. Note this is not the algorithm, but the name specified when adding the encrypter to the CryptoManager. - +3. @Encrypted(encrypter = "") specifies the alias of the encrypter to use for encryption. Note this is not the algorithm, but the name specified when adding the encrypter to the CryptoManager. + +[[example]] === Example .AbstractCouchbaseConfiguration ==== @@ -69,7 +77,7 @@ static class Config extends AbstractCouchbaseConfiguration { @Override public String getBucketName() { /* ... */ } /* provide a cryptoManager */ - @Override + @Override protected CryptoManager cryptoManager() { KeyStore javaKeyStore = KeyStore.getInstance("MyKeyStoreType"); FileInputStream fis = new java.io.FileInputStream("keyStoreName"); @@ -82,6 +90,7 @@ static class Config extends AbstractCouchbaseConfiguration { CryptoManager cryptoManager = DefaultCryptoManager.builder().decrypter(provider.decrypter()) .defaultEncrypter(provider.encrypterForKey("myKey")).build(); + return cryptoManager; } } diff --git a/src/main/asciidoc/reactiverepository.adoc b/src/main/antora/modules/ROOT/pages/couchbase/reactiverepository.adoc similarity index 92% rename from src/main/asciidoc/reactiverepository.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/reactiverepository.adoc index b4f0c3688..25bbc24f8 100644 --- a/src/main/asciidoc/reactiverepository.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/reactiverepository.adoc @@ -5,7 +5,7 @@ == Introduction This chapter describes the reactive repository support for couchbase. -This builds on the core repository support explained in <>. +This builds on the core repository support explained in xref:couchbase/repository.adoc[Couchbase repositories]. So make sure you’ve got a sound understanding of the basic concepts explained there. [[couchbase.reactiverepository.libraries]] @@ -101,4 +101,4 @@ public class PersonRepositoryTests { [[couchbase.reactiverepository.querying]] == Repositories and Querying -Spring Data's Reactive Couchbase comes with full querying support already provided by the blocking <> +Spring Data's Reactive Couchbase comes with full querying support already provided by the blocking xref:couchbase/repository.adoc#couchbase.repository.querying[Repositories and Querying] diff --git a/src/main/asciidoc/repository.adoc b/src/main/antora/modules/ROOT/pages/couchbase/repository.adoc similarity index 85% rename from src/main/asciidoc/repository.adoc rename to src/main/antora/modules/ROOT/pages/couchbase/repository.adoc index 5e3471e3f..df8a32df2 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/antora/modules/ROOT/pages/couchbase/repository.adoc @@ -3,9 +3,10 @@ The goal of Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores. -By default, operations are backed by Key/Value if they are single-document operations and the ID is known. For all other operations by default N1QL queries are generated, and as a result proper indexes must be created for performant data access. +By default, operations are backed by Key/Value if they are single-document operations and the ID is known. +For all other operations by default N1QL queries are generated, and as a result proper indexes must be created for performant data access. -Note that you can tune the consistency you want for your queries (see <>) and have different repositories backed by different buckets (see <>) +Note that you can tune the consistency you want for your queries (see xref:couchbase/repository.adoc#couchbase.repository.consistency[Querying with consistency]) and have different repositories backed by different buckets (see <>) [[couchbase.repository.configuration]] == Configuration @@ -30,6 +31,74 @@ public class Config extends AbstractCouchbaseConfiguration { An advanced usage is described in <>. +[[couchbase.repository.configuration.dsl]] +=== QueryDSL Configuration +Spring Data Couchbase supports QueryDSL for building type-safe queries. To enable code generation, setting `CouchbaseAnnotationProcessor` as an annotation processor is required. +Additionally, the runtime needs querydsl-apt to enable QueryDSL on repositories. + +.Maven Configuration Example +==== +[source,xml] +---- + . existing depdendencies including those required for spring-data-couchbase + . + . + + com.querydsl + querydsl-apt + ${querydslVersion} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + annotation-processing + generate-sources + + compile + + + only + + org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor + + target/generated-sources + + -Aquerydsl.logInfo=true + + + + + + + + +---- +==== + +.Gradle Configuration Example +==== +[source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] +---- +dependencies { + annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}' + annotationProcessor 'org.springframework.data:spring-data-couchbase' + testAnnotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}' + testAnnotationProcessor 'org.springframework.data:spring-data-couchbase' +} +tasks.withType(JavaCompile).configureEach { + options.compilerArgs += [ + "-processor", + "org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor"] +} +---- +==== + [[couchbase.repository.usage]] == Usage @@ -360,6 +429,7 @@ public interface AirportRepository extends PagingAndSortingRepository result = txOperator.execute((ctx) -> ---- ==== +[[transactions-directly-with-the-sdk]] == Transactions Directly with the SDK Spring Data Couchbase works seamlessly with the Couchbase Java SDK for transaction processing. Spring Data Couchbase operations that diff --git a/src/main/antora/modules/ROOT/pages/index.adoc b/src/main/antora/modules/ROOT/pages/index.adoc new file mode 100644 index 000000000..b4ae0a5ea --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/index.adoc @@ -0,0 +1,20 @@ +[[spring-data-couchbase-reference-documentation]] += Spring Data Couchbase +:revnumber: {version} +:revdate: {localdate} +:feature-scroll: true + +_Spring Data Couchbase provides repository support for the Couchbase database. +It eases development of applications with a consistent programming model that need to access Couchbase data sources._ + +[horizontal] +xref:couchbase.adoc[Couchbase] :: Couchbase support and connectivity +xref:repositories.adoc[Repositories] :: Couchbase Repositories +xref:commons/migrating.adoc[Migration] :: Migration Guides +https://github.com/spring-projects/spring-data-commons/wiki[Wiki] :: What's New, Upgrade Notes, Supported Versions, additional cross-version information. + +Michael Nitschinger, Oliver Gierke, Simon Basle, Michael Reiche, Tigran Babloyan + +(C) 2014-{copyright-year} The original author(s) + +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. diff --git a/src/main/antora/modules/ROOT/pages/repositories.adoc b/src/main/antora/modules/ROOT/pages/repositories.adoc new file mode 100644 index 000000000..f7fc60fb1 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories.adoc @@ -0,0 +1,8 @@ +[[couchbase.repositories]] += Repositories +:page-section-summary-toc: 1 + +This chapter explains the basic foundations of Spring Data repositories and Couchbase specifics. +Before continuing to the Couchbase 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. diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc new file mode 100644 index 000000000..579ae7da4 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc @@ -0,0 +1,4 @@ +include::{commons}@data-commons::page$repositories/core-concepts.adoc[] + +[[couchbase.entity-persistence.state-detection-strategies]] +include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1] diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc new file mode 100644 index 000000000..f84313e9d --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/core-domain-events.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc new file mode 100644 index 000000000..a7c2ff8d3 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/core-extensions.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc b/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc new file mode 100644 index 000000000..2ae01801b --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/create-instances.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc new file mode 100644 index 000000000..c7615191a --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/custom-implementations.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/definition.adoc b/src/main/antora/modules/ROOT/pages/repositories/definition.adoc new file mode 100644 index 000000000..bd65a8af8 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/definition.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/definition.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc b/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc new file mode 100644 index 000000000..081bac9f6 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/null-handling.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc new file mode 100644 index 000000000..6168b162d --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/projections.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc new file mode 100644 index 000000000..e495eddc6 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/query-keywords-reference.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc new file mode 100644 index 000000000..dfe481495 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc new file mode 100644 index 000000000..a73c3201d --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc @@ -0,0 +1 @@ +include::{commons}@data-commons::page$repositories/query-return-types-reference.adoc[] diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml new file mode 100644 index 000000000..b6230226c --- /dev/null +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -0,0 +1,19 @@ +version: ${antora-component.version} +prerelease: ${antora-component.prerelease} + +asciidoc: + attributes: + copyright-year: ${current.year} + version: ${project.version} + springversionshort: ${spring.short} + springversion: ${spring} + attribute-missing: 'warn' + commons: ${springdata.commons.docs} + include-xml-namespaces: false + spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference + spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ + spring-framework-docs: https://docs.spring.io/spring-framework/reference/{springversionshort} + spring-framework-javadoc: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api + springhateoasversion: ${spring-hateoas} + releasetrainversion: ${releasetrain} + store: Couchbase diff --git a/src/main/asciidoc/configuration.adoc b/src/main/asciidoc/configuration.adoc deleted file mode 100644 index a57354808..000000000 --- a/src/main/asciidoc/configuration.adoc +++ /dev/null @@ -1,109 +0,0 @@ -[[couchbase.configuration]] -= Installation & Configuration - -This chapter describes the common installation and configuration steps needed when working with the library. - -[[installation]] -== Installation - -All versions intended for production use are distributed across Maven Central and the Spring release repository. -As a result, the library can be included like any other maven dependency: - -.Including the dependency through maven -==== -[source,xml,subs="+attributes"] ----- - - org.springframework.data - spring-data-couchbase - {version} - ----- -==== - -This will pull in several dependencies, including the underlying Couchbase Java SDK, common Spring dependencies and also Jackson as the JSON mapping infrastructure. - -You can also grab snapshots from the https://repo.spring.io/ui/repos/tree/General/libs-snapshot/org/springframework/data/spring-data-couchbase[spring snapshot repository] ( \https://repo.spring.io/libs-snapshot ) and milestone releases from the https://repo.spring.io/ui/repos/tree/General/libs-milestone/org/springframework/data/spring-data-couchbase[spring milestone repository] ( \https://repo.spring.io/libs-milestone ). -Here is an example on how to use the current SNAPSHOT dependency: - -.Using a snapshot version -==== -[source,xml] ----- - - org.springframework.data - spring-data-couchbase - ${version}-SNAPSHOT - - - - spring-libs-snapshot - Spring Snapshot Repository - https://repo.spring.io/libs-snapshot - ----- -==== - -Once you have all needed dependencies on the classpath, you can start configuring it. -Only Java config is supported (XML config has been removed in 4.0). - -[[configuration-java]] -== Annotation-based Configuration ("JavaConfig") - -To get started, all you need to do is subclcass the `AbstractCouchbaseConfiguration` and implement the abstract methods. - -.Extending the `AbstractCouchbaseConfiguration` -==== -[source,java] ----- - -@Configuration -public class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return "couchbase://127.0.0.1"; - } - - @Override - public String getUserName() { - return "Administrator"; - } - - @Override - public String getPassword() { - return "password"; - } - - @Override - public String getBucketName() { - return "travel-sample"; - } -} ----- -==== - -The connection string is made up of a list of hosts and an optional scheme (`couchbase://`) as shown in the code above. -All you need to provide is a list of Couchbase nodes to bootstrap into (separated by a `,`). Please note that while one -host is sufficient in development, it is recommended to add 3 to 5 bootstrap nodes here. Couchbase will pick up all nodes -from the cluster automatically, but it could be the case that the only node you've provided is experiencing issues while -you are starting the application. - -The `userName` and `password` are configured in your Couchbase Server cluster through RBAC (role-based access control). -The `bucketName` reflects the bucket you want to use for this configuration. - -Additionally, the SDK environment can be tuned by overriding the `configureEnvironment` method which takes a -`ClusterEnvironment.Builder` to return a configured `ClusterEnvironment`. - -Many more things can be customized and overridden as custom beans from this configuration (for example repositories, -validation and custom converters). - -TIP: If you use `SyncGateway` and `CouchbaseMobile`, you may run into problem with fields prefixed by `_`. -Since Spring Data Couchbase by default stores the type information as a `_class` attribute this can be problematic. -Override `typeKey()` (for example to return `MappingCouchbaseConverter.TYPEKEY_SYNCGATEWAY_COMPATIBLE`) to change the -name of said attribute. - -If you start your application, you should see Couchbase INFO level logging in the logs, indicating that the underlying -Couchbase Java SDK is connecting to the database. If any errors are reported, make sure that the given credentials -and host information are correct. - diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc deleted file mode 100644 index 9def75779..000000000 --- a/src/main/asciidoc/index.adoc +++ /dev/null @@ -1,43 +0,0 @@ -= Spring Data Couchbase - Reference Documentation -Michael Nitschinger, Oliver Gierke, Simon Baslé, Michael Reiche -:revnumber: {version} -:revdate: {localdate} -:spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc - -(C) 2014-2022 The original author(s). - -NOTE: 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. - -toc::[] - -include::preface.adoc[] -include::{spring-data-commons-docs}/upgrade.adoc[leveloffset=+1] - -[[reference]] -= Reference Documentation - -:leveloffset: +1 -include::configuration.adoc[] -include::entity.adoc[] -include::autokeygeneration.adoc[] -include::{spring-data-commons-docs}/repositories.adoc[] -include::repository.adoc[] -include::reactiverepository.adoc[] -include::template.adoc[] -include::transactions.adoc[] -include::collections.adoc[] -include::fieldlevelencryption.adoc[] -include::ansijoins.adoc[] -include::caching.adoc[] -:leveloffset: -1 - -[[appendix]] -= Appendix - -:numbered!: -:leveloffset: +1 -include::{spring-data-commons-docs}/repository-namespace-reference.adoc[] -include::{spring-data-commons-docs}/repository-populator-namespace-reference.adoc[] -include::{spring-data-commons-docs}/repository-query-keywords-reference.adoc[] -include::{spring-data-commons-docs}/repository-query-return-types-reference.adoc[] -:leveloffset: -1 diff --git a/src/main/asciidoc/preface.adoc b/src/main/asciidoc/preface.adoc deleted file mode 100644 index 395c67e03..000000000 --- a/src/main/asciidoc/preface.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[couchbase.preface]] -= Preface - -This reference documentation describes the general usage of the Spring Data Couchbase library. - -[[metadata]] -[preface] -== Project Information - -* Version control - https://github.com/spring-projects/spring-data-couchbase -* Bugtracker - https://jira.springsource.org/browse/DATACOUCH -* Release repository - https://repo.spring.io/libs-release -* Milestone repository - https://repo.spring.io/libs-milestone -* Snapshot repository - https://repo.spring.io/libs-snapshot - -[preface] -include::migrating.adoc[leveloffset=+1] - diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java deleted file mode 100644 index 05c0e5596..000000000 --- a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java +++ /dev/null @@ -1,34 +0,0 @@ -/* -/* - * Copyright 2021-2023 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 com.couchbase.client.java.transactions; - -import com.couchbase.client.core.annotation.Stability; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.java.codec.JsonSerializer; - -/** - * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext - * - * @author Michael Reiche - */ -@Stability.Internal -public class AttemptContextReactiveAccessor { - public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext( - CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { - return new ReactiveTransactionAttemptContext(core, jsonSerializer); - } -} diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 1901a0cd1..dc2a9ed5a 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index 06cb5c5c0..3e82d71d0 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/cache/CacheKeyPrefix.java b/src/main/java/org/springframework/data/couchbase/cache/CacheKeyPrefix.java index 96a47478a..64669acbf 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CacheKeyPrefix.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CacheKeyPrefix.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCache.java b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCache.java index cc5944046..8ee5780e0 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCache.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.cache; + import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -23,7 +24,6 @@ import java.util.concurrent.Callable; import org.springframework.cache.support.AbstractValueAdaptingCache; -import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -31,6 +31,13 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +/** + * Couchbase-backed Cache Methods that take a Class return non-wrapped objects - cache-miss cannot be distinguished from + * cached null - this is what AbstractValueAdaptingCache does Methods that do not take a Class return wrapped objects - + * the wrapper is null for cache-miss - the exception is T get(final Object key, final Callable<T> valueLoader), which + * does not return a wrapper because if there is a cache-miss, it gets the value from valueLoader (and caches it). There + * are anomalies with get(key, ValueLoader) - which returns non-wrapped object. + */ public class CouchbaseCache extends AbstractValueAdaptingCache { private final String name; @@ -70,11 +77,18 @@ public CouchbaseCacheWriter getNativeCache() { return cacheWriter; } + /** + * same as inherited, but passes clazz for transcoder + */ + protected Object lookup(final Object key, Class clazz) { + return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder(), + clazz); + } + @Override protected Object lookup(final Object key) { - return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder()); + return lookup(key, Object.class); } - /** * Returns the configuration for this {@link CouchbaseCache}. */ @@ -97,33 +111,56 @@ public synchronized T get(final Object key, final Callable valueLoader) { } @Override - public void put(final Object key, final Object value) { - if (!isAllowNullValues() && value == null) { + @SuppressWarnings("unchecked") + public T get(final Object key, Class type) { + Object value = this.fromStoreValue(this.lookup(key, type)); + if (value != null && type != null && !type.isInstance(value)) { + throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value); + } else { + return (T) value; + } + } - throw new IllegalArgumentException(String.format( - "Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or " - + "configure CouchbaseCache to allow 'null' via CouchbaseCacheConfiguration.", - name)); + public synchronized T get(final Object key, final Callable valueLoader, Class type) { + T value = get(key, type); + if (value == null) { // cannot distinguish between cache miss and cached null + value = valueFromLoader(key, valueLoader); + put(key, value); } + return value; + } + @Override + public void put(final Object key, final Object value) { cacheWriter.put(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value), cacheConfig.getExpiry(), cacheConfig.getValueTranscoder()); } @Override public ValueWrapper putIfAbsent(final Object key, final Object value) { - if (!isAllowNullValues() && value == null) { - return get(key); - } Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value), cacheConfig.getExpiry(), cacheConfig.getValueTranscoder()); - if (result == null) { - return null; - } + return toValueWrapper(result); + } - return new SimpleValueWrapper(result); + /** + * Not sure why this isn't in AbstractValueAdaptingCache + * + * @param key + * @param value + * @param clazz + * @return + * @param + */ + @SuppressWarnings("unchecked") + public T putIfAbsent(final Object key, final Object value, final Class clazz) { + + Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key), + toStoreValue(value), cacheConfig.getExpiry(), cacheConfig.getValueTranscoder(), clazz); + + return (T) result; } @Override @@ -168,6 +205,9 @@ protected String createCacheKey(final Object key) { * @throws IllegalStateException if {@code key} cannot be converted to {@link String}. */ protected String convertKey(final Object key) { + if (key == null) { + throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' key.", name)); + } if (key instanceof String) { return (String) key; } diff --git a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java index 625e42e56..a618c66ca 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -62,7 +62,7 @@ public static CouchbaseCacheConfiguration defaultCacheConfig() { /** * Registers default cache key converters. The following converters get registered: *
    - *
  • {@link String} to {@link byte byte[]} using UTF-8 encoding.
  • + *
  • {@link String} to byte using UTF-8 encoding.
  • *
  • {@link SimpleKey} to {@link String}
  • *
* diff --git a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheManager.java b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheManager.java index 4c296ebe6..e18e09c23 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheManager.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -28,7 +28,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; public class CouchbaseCacheManager extends AbstractTransactionSupportingCacheManager { diff --git a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheWriter.java b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheWriter.java index cc80e20a7..55450d7ca 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheWriter.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -18,7 +18,7 @@ import java.time.Duration; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.couchbase.client.java.codec.Transcoder; @@ -48,6 +48,20 @@ public interface CouchbaseCacheWriter { Object putIfAbsent(String collectionName, String key, Object value, @Nullable Duration expiry, @Nullable Transcoder transcoder); + /** + * Write the given value to Couchbase if the key does not already exist. + * + * @param collectionName The cache name must not be {@literal null}. + * @param key The key for the cache entry. Must not be {@literal null}. + * @param value The value stored for the key. Must not be {@literal null}. + * @param expiry Optional expiration time. Can be {@literal null}. + * @param transcoder Optional transcoder to use. Can be {@literal null}. + * @param clazz Optional class for contentAs(clazz) + */ + @Nullable + Object putIfAbsent(String collectionName, String key, Object value, @Nullable Duration expiry, + @Nullable Transcoder transcoder, @Nullable Class clazz); + /** * Get the binary value representation from Couchbase stored for the given key. * @@ -59,6 +73,18 @@ Object putIfAbsent(String collectionName, String key, Object value, @Nullable Du @Nullable Object get(String collectionName, String key, @Nullable Transcoder transcoder); + /** + * Get the binary value representation from Couchbase stored for the given key. + * + * @param collectionName must not be {@literal null}. + * @param key must not be {@literal null}. + * @param transcoder Optional transcoder to use. Can be {@literal null}. + * @param clazz Optional class for contentAs(clazz) + * @return {@literal null} if key does not exist. + */ + @Nullable + Object get(String collectionName, String key, @Nullable Transcoder transcoder, @Nullable Class clazz); + /** * Remove the given key from Couchbase. * diff --git a/src/main/java/org/springframework/data/couchbase/cache/DefaultCouchbaseCacheWriter.java b/src/main/java/org/springframework/data/couchbase/cache/DefaultCouchbaseCacheWriter.java index 635d0792f..20b6c9f5b 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/DefaultCouchbaseCacheWriter.java +++ b/src/main/java/org/springframework/data/couchbase/cache/DefaultCouchbaseCacheWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -18,12 +18,14 @@ import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; -import static com.couchbase.client.java.kv.GetOptions.*; -import static com.couchbase.client.java.kv.InsertOptions.*; -import static com.couchbase.client.java.kv.UpsertOptions.*; -import static com.couchbase.client.java.query.QueryOptions.*; +import static com.couchbase.client.java.kv.GetOptions.getOptions; +import static com.couchbase.client.java.kv.InsertOptions.insertOptions; +import static com.couchbase.client.java.kv.UpsertOptions.upsertOptions; +import static com.couchbase.client.java.query.QueryOptions.queryOptions; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import io.micrometer.common.lang.Nullable; + import java.time.Duration; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -65,6 +67,22 @@ public void put(final String collectionName, final String key, final Object valu @Override public Object putIfAbsent(final String collectionName, final String key, final Object value, final Duration expiry, final Transcoder transcoder) { + return putIfAbsent(collectionName, key, value, expiry, transcoder, Object.class); + } + + /** + * same as above, plus clazz + * + * @param collectionName + * @param key + * @param value + * @param expiry + * @param transcoder + * @param clazz + */ + @Override + public Object putIfAbsent(final String collectionName, final String key, final Object value, final Duration expiry, + final Transcoder transcoder, @Nullable final Class clazz) { InsertOptions options = insertOptions(); if (expiry != null) { @@ -79,15 +97,20 @@ public Object putIfAbsent(final String collectionName, final String key, final O return null; } catch (final DocumentExistsException ex) { // If the document exists, return the current one per contract - return get(collectionName, key, transcoder); + return get(collectionName, key, transcoder, clazz); } } @Override public Object get(final String collectionName, final String key, final Transcoder transcoder) { - // TODO .. the decoding side transcoding needs to be figured out? + return get(collectionName, key, transcoder, Object.class); + } + + @Override + public Object get(final String collectionName, final String key, final Transcoder transcoder, + final Class clazz) { try { - return getCollection(collectionName).get(key, getOptions().transcoder(transcoder)).contentAs(Object.class); + return getCollection(collectionName).get(key, getOptions().transcoder(transcoder)).contentAs(clazz); } catch (DocumentNotFoundException ex) { return null; } diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 9eacc4c26..3b24aaa8d 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -26,12 +26,15 @@ import java.util.Map; import java.util.Set; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.PropertyValueConverterRegistrar; @@ -94,6 +97,7 @@ * @author Subhashni Balakrishnan * @author Jorge Rodriguez Martin * @author Michael Reiche + * @author Vipul Gupta */ @Configuration public abstract class AbstractCouchbaseConfiguration { @@ -274,8 +278,7 @@ public String typeKey() { @Bean public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, CouchbaseCustomConversions couchbaseCustomConversions) { - MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey()); - converter.setCustomConversions(couchbaseCustomConversions); + MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey(), couchbaseCustomConversions); couchbaseMappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder()); return converter; } @@ -374,19 +377,32 @@ public CouchbaseTransactionalOperator couchbaseTransactionalOperator( return CouchbaseTransactionalOperator.create(couchbaseCallbackTransactionManager); } - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public TransactionInterceptor transactionInterceptor(TransactionManager couchbaseTransactionManager) { - TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); - TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(couchbaseTransactionManager, - transactionAttributeSource); - interceptor.setTransactionAttributeSource(transactionAttributeSource); - if (couchbaseTransactionManager != null) { - interceptor.setTransactionManager(couchbaseTransactionManager); - } - return interceptor; - } - + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static BeanPostProcessor transactionInterceptorCustomizer( + ObjectProvider transactionManagerProvider, ConfigurableListableBeanFactory beanFactory) { + + BeanPostProcessor processor = new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof TransactionInterceptor) { + TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); + TransactionManager transactionManager = transactionManagerProvider.getObject(); + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(transactionManager, + transactionAttributeSource); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + if (transactionManager != null) { + interceptor.setTransactionManager(transactionManager); + } + return interceptor; + } + return bean; + } + + }; + beanFactory.addBeanPostProcessor(processor); + return processor; + } /** * Configure whether to automatically create indices for domain types by deriving the from the entity or not. */ @@ -412,10 +428,17 @@ public CustomConversions customConversions() { * and {@link #couchbaseMappingContext(CustomConversions)}. * * @param cryptoManager + * @param objectMapper * @return must not be {@literal null}. */ public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper) { - List newConverters = new ArrayList(); + List newConverters = new ArrayList(); + // The following + newConverters.add(new OtherConverters.EnumToObject(getObjectMapper())); + newConverters.add(new IntegerToEnumConverterFactory(getObjectMapper())); + newConverters.add(new StringToEnumConverterFactory(getObjectMapper())); + newConverters.add(new BooleanToEnumConverterFactory(getObjectMapper())); + additionalConverters(newConverters); CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> { SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions(); valueConversions.setConverterFactory( @@ -424,15 +447,18 @@ public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMa valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory configurationAdapter.setPropertyValueConversions(valueConversions); configurationAdapter.registerConverters(newConverters); - configurationAdapter.registerConverter(new OtherConverters.EnumToObject(getObjectMapper())); - configurationAdapter.registerConverterFactory(new IntegerToEnumConverterFactory(getObjectMapper())); - configurationAdapter.registerConverterFactory(new StringToEnumConverterFactory(getObjectMapper())); - configurationAdapter.registerConverterFactory(new BooleanToEnumConverterFactory(getObjectMapper())); }); return customConversions; } - Map, Class> annotationToConverterMap() { + /** + * This should be overridden in order to update the {@link #customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper)} List + */ + protected void additionalConverters(List converters) { + // NO_OP + } + + public static Map, Class> annotationToConverterMap() { Map, Class> map = new HashMap(); map.put(Encrypted.class, CryptoConverter.class); map.put(JsonValue.class, JsonValueConverter.class); diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index 1c5966788..25b6258df 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 3b34561e1..967b9db75 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CollectionCallback.java b/src/main/java/org/springframework/data/couchbase/core/CollectionCallback.java index e9beb9d82..4913f597b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CollectionCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/CollectionCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseDataIntegrityViolationException.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseDataIntegrityViolationException.java index 8c309e664..eeac8dda3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseDataIntegrityViolationException.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseDataIntegrityViolationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java index 9fa0c7840..60666625b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index dde7626cf..541c07439 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseQueryExecutionException.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseQueryExecutionException.java index d247be273..12b7f767b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseQueryExecutionException.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseQueryExecutionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 4bca6e330..0d6aabee4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -30,10 +30,13 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.couchbase.client.java.Collection; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import reactor.core.publisher.Mono; /** * Implements lower-level couchbase operations on top of the SDK with entity mapping capabilities. @@ -83,8 +86,49 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final Couch @Override public T save(T entity, String... scopeAndCollection) { - return reactive().save(entity, scopeAndCollection).block(); - } + Assert.notNull(entity, "Entity must not be null!"); + + String scope = scopeAndCollection.length > 0 ? scopeAndCollection[0] : null; + String collection = scopeAndCollection.length > 1 ? scopeAndCollection[1] : null; + final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() + .getPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); + final boolean versionPresent = versionProperty != null; + final Long version = versionProperty == null || versionProperty.getField() == null ? null + : (Long) ReflectionUtils.getField(versionProperty.getField(), + entity); + final boolean existingDocument = version != null && version > 0; + + Class clazz = entity.getClass(); + + if (!versionPresent) { // the entity doesn't have a version property + // No version field - no cas + // If in a transaction, insert is the only thing that will work + return (T)TransactionalSupport.checkForTransactionInThreadLocalStorage() + .map(ctx -> { + if (ctx.isPresent()) { + return (T) insertById(clazz).inScope(scope) + .inCollection(collection) + .one(entity); + } else { // if not in a tx, then upsert will work + return (T) upsertById(clazz).inScope(scope) + .inCollection(collection) + .one(entity); + } + }).block(); + + } else if (existingDocument) { // there is a version property, and it is non-zero + // Updating existing document with cas + return (T)replaceById(clazz).inScope(scope) + .inCollection(collection) + .one(entity); + } else { // there is a version property, but it's zero or not set. + // Creating new document + return (T)insertById(clazz).inScope(scope) + .inCollection(collection) + .one(entity); + } + } @Override public Long count(Query query, Class domainType) { @@ -158,6 +202,11 @@ public ExecutableRemoveByQuery removeByQuery(Class domainType) { return new ExecutableRemoveByQueryOperationSupport(this).removeByQuery(domainType); } + @Override + public ExecutableRangeScan rangeScan(Class domainType) { + return new ExecutableRangeScanOperationSupport(this).rangeScan(domainType); + } + @Override public String getBucketName() { return clientFactory.getBucket().name(); diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 9b928cc3f..05277c691 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java index 0d1609c94..c454dfac4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java index 15cf29746..5dfc2429e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index b57525e14..4b3b3331a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.support.WithAnalyticsConsistency; import org.springframework.data.couchbase.core.support.WithAnalyticsOptions; import org.springframework.data.couchbase.core.support.WithAnalyticsQuery; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.analytics.AnalyticsScanConsistency; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index 19418f5b8..bfe71e111 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 5c7f6095b..2be2c338b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 22b95284e..e1bf1305b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 835912c12..51ef069af 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -29,7 +29,7 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 0bfd712c1..e201f2e15 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java index b5923c094..9a57edcb9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java index 1bd91f63c..cdb1308e3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 0f41962ec..77e926e98 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index f1c78a26f..daf40b847 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -40,7 +40,7 @@ public ExecutableInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java index 208d65f03..f4c711123 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java index 36a5eed8d..fee04bd2d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -51,7 +51,7 @@ public ExecutableMutateInById mutateInById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableMutateInByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperation.java new file mode 100644 index 000000000..684b6d5eb --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperation.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-2025 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.couchbase.core; + +import java.util.stream.Stream; + +import org.springframework.data.couchbase.core.support.ConsistentWith; +import org.springframework.data.couchbase.core.support.InCollection; +import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithBatchByteLimit; +import org.springframework.data.couchbase.core.support.WithBatchItemLimit; +import org.springframework.data.couchbase.core.support.WithScanOptions; +import org.springframework.data.couchbase.core.support.WithScanSort; + +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.ScanOptions; + +/** + * Get Operations + * + * @author Michael Reiche + */ +public interface ExecutableRangeScanOperation { + + /** + * Loads a document from a bucket. + * + * @param domainType the entity type to use for the results. + */ + ExecutableRangeScan rangeScan(Class domainType); + + /** + * Terminating operations invoking the actual execution. + * + * @param the entity type to use for the results. + */ + interface TerminatingRangeScan /*extends OneAndAllId*/ { + + /** + * Range Scan + * + * @param upper + * @param lower + * @return the list of found entities. + */ + Stream rangeScan(String lower, String upper); + + /** + * Range Scan Ids + * + * @param upper + * @param lower + * @return the list of found keys. + */ + Stream rangeScanIds(String lower, String upper); + + /** + * Range Scan + * + * @param limit + * @param seed + * @return the list of found entities. + */ + Stream samplingScan(Long limit, Long... seed); + + /** + * Range Scan Ids + * + * @param limit + * @param seed + * @return the list of keys + */ + Stream samplingScanIds(Long limit, Long... seed); + } + + /** + * Fluent method to specify options. + * + * @param the entity type to use for the results. + */ + interface RangeScanWithOptions extends TerminatingRangeScan, WithScanOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options options to use for execution + */ + @Override + TerminatingRangeScan withOptions(ScanOptions options); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface RangeScanInCollection extends RangeScanWithOptions, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + RangeScanWithOptions inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface RangeScanInScope extends RangeScanInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + RangeScanInCollection inScope(String scope); + } + + + + interface RangeScanWithSort extends RangeScanInScope, WithScanSort { + /** + * sort + * + * @param sort + */ + @Override + RangeScanInScope withSort(Object sort); + } + + /** + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * + * @param the entity type to use for the results. + */ + interface RangeScanConsistentWith extends RangeScanWithSort, ConsistentWith { + + /** + * Allows to override the default scan consistency. + * + * @param mutationState the custom scan consistency to use for this query. + */ + @Override + RangeScanWithSort consistentWith(MutationState mutationState); + } + + /** + * Fluent method to specify a return type different than the the entity type to use for the results. + * + * @param the entity type to use for the results. + */ + interface RangeScanWithProjection extends RangeScanConsistentWith { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are only interested in the original the entity type to use for the results. + * + * @param returnType must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQueryOperation.FindByQueryWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + RangeScanConsistentWith as(Class returnType); + } + + interface RangeScanWithBatchItemLimit extends RangeScanWithProjection, WithBatchItemLimit { + + /** + * determines if result are just ids or ids plus contents + * + * @param batchByteLimit must not be {@literal null}. + * @return new instance of {@link RangeScanWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + @Override + RangeScanWithProjection withBatchItemLimit(Integer batchByteLimit); + } + + interface RangeScanWithBatchByteLimit extends RangeScanWithBatchItemLimit, WithBatchByteLimit { + + /** + * determines if result are just ids or ids plus contents + * + * @param batchByteLimit must not be {@literal null}. + * @return new instance of {@link RangeScanWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + @Override + RangeScanWithBatchItemLimit withBatchByteLimit(Integer batchByteLimit); + } + + /** + * Provides methods for constructing query operations in a fluent way. + * + * @param the entity type to use for the results + */ + interface ExecutableRangeScan extends RangeScanWithBatchByteLimit {} + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperationSupport.java new file mode 100644 index 000000000..656aaa819 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRangeScanOperationSupport.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2025 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.couchbase.core; + +import java.util.stream.Stream; + +import org.springframework.data.couchbase.core.ReactiveRangeScanOperationSupport.ReactiveRangeScanSupport; +import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.util.Assert; + +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.ScanOptions; + +public class ExecutableRangeScanOperationSupport implements ExecutableRangeScanOperation { + + private final CouchbaseTemplate template; + + ExecutableRangeScanOperationSupport(CouchbaseTemplate template) { + this.template = template; + } + + @Override + public ExecutableRangeScan rangeScan(Class domainType) { + return new ExecutableRangeScanSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), + OptionsBuilder.getCollectionFrom(domainType), null, null, null, null, null); + } + + static class ExecutableRangeScanSupport implements ExecutableRangeScan { + + private final CouchbaseTemplate template; + private final Class domainType; + private final String scope; + private final String collection; + private final ScanOptions options; + private final Object sort; + private final MutationState mutationState; + private final Integer batchItemLimit; + private final Integer batchByteLimit; + private final ReactiveRangeScanSupport reactiveSupport; + + ExecutableRangeScanSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, + ScanOptions options, Object sort, MutationState mutationState, + Integer batchItemLimit, Integer batchByteLimit) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.sort = sort; + this.mutationState = mutationState; + this.batchItemLimit = batchItemLimit; + this.batchByteLimit = batchByteLimit; + this.reactiveSupport = new ReactiveRangeScanSupport<>(template.reactive(), domainType, scope, collection, options, + sort, mutationState, batchItemLimit, batchByteLimit, + new NonReactiveSupportWrapper(template.support())); + } + + @Override + public TerminatingRangeScan withOptions(final ScanOptions options) { + Assert.notNull(options, "Options must not be null."); + return new ExecutableRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanWithOptions inCollection(final String collection) { + return new ExecutableRangeScanSupport<>(template, domainType, scope, + collection != null ? collection : this.collection, options, sort, mutationState, + batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanInCollection inScope(final String scope) { + return new ExecutableRangeScanSupport<>(template, domainType, scope != null ? scope : this.scope, collection, + options, sort, mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanInScope withSort(Object sort) { + return new ExecutableRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanWithSort consistentWith(MutationState mutationState) { + return new ExecutableRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanConsistentWith as(Class returnType) { + return new ExecutableRangeScanSupport<>(template, returnType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanWithProjection withBatchItemLimit(Integer batchItemLimit) { + return new ExecutableRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public RangeScanWithBatchItemLimit withBatchByteLimit(Integer batchByteLimit) { + return new ExecutableRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit); + } + + @Override + public Stream rangeScan(String lower, String upper) { + return reactiveSupport.rangeScan(lower, upper, false, null, null).toStream(); + } + + @Override + public Stream rangeScanIds(String lower, String upper) { + return reactiveSupport.rangeScanIds(lower, upper, false, null, null).toStream(); + } + + @Override + public Stream samplingScan(Long limit, Long... seed) { + return reactiveSupport.sampleScan(limit, seed).toStream(); + } + + @Override + public Stream samplingScanIds(Long limit, Long... seed) { + return reactiveSupport.sampleScanIds(limit, seed).toStream(); + } + + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 78360dcaf..0dbed2a65 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index fa1d0afe5..95a6d775a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -52,7 +52,7 @@ public ExecutableRemoveById removeById(Class domainType) { return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index c725d1643..65cc00e1d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 8cbbd7457..da04c6ab7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index d90bd9f96..80be9505f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 5151a039d..65d9b3a5a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -40,7 +40,7 @@ public ExecutableReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index 1b62fa10f..c808efa28 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java index bd5a71b36..fbeb540a5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -40,7 +40,7 @@ public ExecutableUpsertById upsertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java index eee5dc0bd..b799c0c5f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -22,4 +22,5 @@ public interface FluentCouchbaseOperations extends ExecutableUpsertByIdOperation, ExecutableInsertByIdOperation, ExecutableReplaceByIdOperation, ExecutableFindByIdOperation, ExecutableFindFromReplicasByIdOperation, ExecutableFindByQueryOperation, ExecutableFindByAnalyticsOperation, ExecutableExistsByIdOperation, - ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation, ExecutableMutateInByIdOperation {} + ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation, ExecutableMutateInByIdOperation, + ExecutableRangeScanOperation {} diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index c67351225..4c19404c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/OperationCancellationException.java b/src/main/java/org/springframework/data/couchbase/core/OperationCancellationException.java index fe93d1e19..4c145fd33 100644 --- a/src/main/java/org/springframework/data/couchbase/core/OperationCancellationException.java +++ b/src/main/java/org/springframework/data/couchbase/core/OperationCancellationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/OperationInterruptedException.java b/src/main/java/org/springframework/data/couchbase/core/OperationInterruptedException.java index bc578579e..a1e39ca1c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/OperationInterruptedException.java +++ b/src/main/java/org/springframework/data/couchbase/core/OperationInterruptedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index fb0ddfb18..64e2c9197 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 3a032586b..c41e9d2ef 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -45,6 +45,7 @@ * @author Jorge Rodriguez Martin * @author Carlos Espinaco * @author Tigran Babloyan + * @author Andy Toone */ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware { @@ -52,7 +53,7 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A private final CouchbaseConverter converter; private final PersistenceExceptionTranslator exceptionTranslator; private final ReactiveCouchbaseTemplateSupport templateSupport; - private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); + private final ThreadLocal> threadLocalArgs = new ThreadLocal<>(); private final QueryScanConsistency scanConsistency; public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { @@ -79,7 +80,7 @@ public Mono save(T entity, String... scopeAndCollection) { String scope = scopeAndCollection.length > 0 ? scopeAndCollection[0] : null; String collection = scopeAndCollection.length > 1 ? scopeAndCollection[1] : null; - return Mono.defer(() -> { + return Mono.deferContextual( ctx1 -> { final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() .getPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); @@ -189,6 +190,11 @@ public ReactiveMutateInById mutateInById(Class domainType) { return new ReactiveMutateInByIdOperationSupport(this).mutateInById(domainType); } + @Override + public ReactiveRangeScan rangeScan(Class domainType) { + return new ReactiveRangeScanOperationSupport(this).rangeScan(domainType); + } + @Override public String getBucketName() { return clientFactory.getBucket().name(); @@ -251,7 +257,6 @@ public PseudoArgs getPseudoArgs() { * set the ThreadLocal field */ public void setPseudoArgs(PseudoArgs threadLocalArgs) { - this.threadLocalArgs = new ThreadLocal<>(); this.threadLocalArgs.set(threadLocalArgs); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 42a26c2d1..558eed09e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java index f24560acb..6954a06c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 42caf653d..f8e03ea4c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index 9d40f49a1..5fe94a070 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index 1be52c4db..aab918318 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 59ae53fce..4b0d3a07c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 81fcf6a36..6a848aa69 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index 3d4e10cc3..f4479c13a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index f8647066a..523d882b3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.api.query.CoreQueryContext; +import com.couchbase.client.core.api.query.CoreQueryOptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,13 +29,11 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; -import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.codec.JsonSerializer; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import com.couchbase.client.java.transactions.TransactionQueryOptions; import com.couchbase.client.java.transactions.TransactionQueryResult; @@ -76,9 +76,9 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, + final String scope, final String collection, final QueryOptions options, final String[] distinctFields, + final String[] fields, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -191,10 +191,16 @@ public Flux all() { return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) : rs.query(statement, opts); } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + TransactionQueryOptions options = buildTransactionOptions(pArgs.getOptions()); JsonSerializer jSer = clientFactory.getCluster().environment().jsonSerializer(); - return AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), jSer) - .query(rs.name().equals(CollectionIdentifier.DEFAULT_SCOPE) ? null : rs, statement, opts); + CoreQueryOptions opts = options != null ? options.builder().build() : null; + return s.get().getCore() + .queryBlocking(statement, + pArgs.getScope() == null ? null + : CoreQueryContext.of(rs.bucketName(), pArgs.getScope()), + opts, false) + .map(response -> new TransactionQueryResult(response, jSer)); + } }); @@ -229,7 +235,7 @@ public Flux all() { public QueryOptions buildOptions(QueryOptions options) { QueryScanConsistency qsc = scanConsistency != null ? scanConsistency : template.getConsistency(); - return query.buildQueryOptions(options, qsc); + return query.buildQueryOptions(options, qsc).readonly(query.isReadonly()); } private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { @@ -254,9 +260,15 @@ public Mono count() { return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) : rs.query(statement, opts); } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), - clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); + TransactionQueryOptions options = buildTransactionOptions(pArgs.getOptions()); + JsonSerializer jSer = clientFactory.getCluster().environment().jsonSerializer(); + CoreQueryOptions opts = options != null ? options.builder().build() : null; + return s.get().getCore() + .queryBlocking(statement, + pArgs.getScope() == null ? null + : CoreQueryContext.of(rs.bucketName(), pArgs.getScope()), + opts, false) + .map(response -> new TransactionQueryResult(response, jSer)); } }); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java index fb5112e1c..aa1c94019 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 2a4b5aae8..a93302752 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java index d7652642c..7a55a44c2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -22,4 +22,5 @@ public interface ReactiveFluentCouchbaseOperations extends ReactiveUpsertByIdOperation, ReactiveInsertByIdOperation, ReactiveReplaceByIdOperation, ReactiveFindByIdOperation, ReactiveExistsByIdOperation, ReactiveFindByAnalyticsOperation, ReactiveFindFromReplicasByIdOperation, ReactiveFindByQueryOperation, - ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation, ReactiveMutateInByIdOperation {} + ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation, ReactiveMutateInByIdOperation, + ReactiveRangeScanOperation {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 3a212e351..3ef286848 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 2c71a0910..28add4a4b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -61,7 +61,7 @@ public ReactiveInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java index 7be0520be..6ec3df7d2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java index f44c871f3..3dc1fd4f2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -52,7 +52,7 @@ public ReactiveMutateInById mutateInById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveMutateInByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null, template.support(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperation.java new file mode 100644 index 000000000..183dc1a21 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperation.java @@ -0,0 +1,212 @@ +/* + * Copyright 2012-2025 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.couchbase.core; + +import reactor.core.publisher.Flux; + +import org.springframework.data.couchbase.core.support.ConsistentWith; +import org.springframework.data.couchbase.core.support.InCollection; +import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithBatchByteLimit; +import org.springframework.data.couchbase.core.support.WithBatchItemLimit; +import org.springframework.data.couchbase.core.support.WithScanOptions; +import org.springframework.data.couchbase.core.support.WithScanSort; + +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.ScanOptions; + +/** + * Get Operations + * + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveRangeScanOperation { + + /** + * Loads a document from a bucket. + * + * @param domainType the entity type to use for the results. + */ + ReactiveRangeScan rangeScan(Class domainType); + + /** + * Terminating operations invoking the actual execution. + * + * @param the entity type to use for the results. + */ + interface TerminatingRangeScan /*extends OneAndAllId*/ { + + /** + * Finds a list of documents based on the given IDs. + * + * @param lower the lower bound + * @param upper the upper bound + * @return the list of found entities. + */ + Flux rangeScan(String lower, String upper); + + /** + * Finds a list of documents based on the given IDs. + * + * @param lower the lower bound + * @param upper the upper bound + * @return the list of ids. + */ + Flux rangeScanIds(String lower, String upper); + + /** + * Finds a list of documents based on the given IDs. + * + * @param limit + * @param seed + * @return the list of found entities. + */ + Flux sampleScan(Long limit, Long... seed); + + /** + * Finds a list of documents based on the given IDs. + * + * @param limit + * @param seed + * @return the list of ids. + */ + Flux sampleScanIds(Long limit, Long... seed); + } + + /** + * Fluent method to specify options. + * + * @param the entity type to use for the results. + */ + interface RangeScanWithOptions extends TerminatingRangeScan, WithScanOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options options to use for execution + */ + @Override + TerminatingRangeScan withOptions(ScanOptions options); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface RangeScanInCollection extends RangeScanWithOptions, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + RangeScanWithOptions inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface RangeScanInScope extends RangeScanInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + RangeScanInCollection inScope(String scope); + } + + interface RangeScanWithSort extends RangeScanInScope, WithScanSort { + /** + * sort + * + * @param sort + */ + @Override + RangeScanInScope withSort(Object sort); + } + + /** + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * + * @param the entity type to use for the results. + */ + interface RangeScanConsistentWith extends RangeScanWithSort, ConsistentWith { + + /** + * Allows to override the default scan consistency. + * + * @param mutationState the custom scan consistency to use for this query. + */ + @Override + RangeScanWithSort consistentWith(MutationState mutationState); + } + + /** + * Fluent method to specify a return type different than the the entity type to use for the results. + * + * @param the entity type to use for the results. + */ + interface RangeScanWithProjection extends RangeScanConsistentWith { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are only interested in the original the entity type to use for the results. + * + * @param returnType must not be {@literal null}. + * @return new instance of {@link ReactiveFindByQueryOperation.FindByQueryWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + RangeScanConsistentWith as(Class returnType); + } + + interface RangeScanWithBatchItemLimit extends RangeScanWithProjection, WithBatchItemLimit { + + /** + * determines if result are just ids or ids plus contents + * + * @param batchByteLimit must not be {@literal null}. + * @return new instance of {@link RangeScanWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + @Override + RangeScanWithProjection withBatchItemLimit(Integer batchByteLimit); + } + + interface RangeScanWithBatchByteLimit extends RangeScanWithBatchItemLimit, WithBatchByteLimit { + + /** + * determines if result are just ids or ids plus contents + * + * @param batchByteLimit must not be {@literal null}. + * @return new instance of {@link RangeScanWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + @Override + RangeScanWithBatchItemLimit withBatchByteLimit(Integer batchByteLimit); + } + + /** + * Provides methods for constructing query operations in a fluent way. + * + * @param the entity type to use for the results + */ + interface ReactiveRangeScan extends RangeScanWithBatchByteLimit {} + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java new file mode 100644 index 000000000..8e0c825e4 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java @@ -0,0 +1,230 @@ +/* + * Copyright 2012-2025 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.couchbase.core; + +import reactor.core.publisher.Flux; + +import java.nio.charset.StandardCharsets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.util.Assert; + +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.ScanOptions; +import com.couchbase.client.java.kv.ScanTerm; +import com.couchbase.client.java.kv.ScanType; + +public class ReactiveRangeScanOperationSupport implements ReactiveRangeScanOperation { + + private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveRangeScanOperationSupport.class); + + ReactiveRangeScanOperationSupport(ReactiveCouchbaseTemplate template) { + this.template = template; + } + + @Override + public ReactiveRangeScan rangeScan(Class domainType) { + return new ReactiveRangeScanSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), + OptionsBuilder.getCollectionFrom(domainType), null, null, null, null, null, + template.support()); + } + + static class ReactiveRangeScanSupport implements ReactiveRangeScan { + + private final ReactiveCouchbaseTemplate template; + private final Class domainType; + private final String scope; + private final String collection; + private final ScanOptions options; + private final Object sort; + private final MutationState mutationState; + private final Integer batchItemLimit; + private final Integer batchByteLimit; + private final ReactiveTemplateSupport support; + + ReactiveRangeScanSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, + ScanOptions options, Object sort, MutationState mutationState, + Integer batchItemLimit, Integer batchByteLimit, ReactiveTemplateSupport support) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.sort = sort; + this.mutationState = mutationState; + this.batchItemLimit = batchItemLimit; + this.batchByteLimit = batchByteLimit; + this.support = support; + } + + @Override + public TerminatingRangeScan withOptions(final ScanOptions options) { + Assert.notNull(options, "Options must not be null."); + return new ReactiveRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanWithOptions inCollection(final String collection) { + return new ReactiveRangeScanSupport<>(template, domainType, scope, + collection != null ? collection : this.collection, options, sort, mutationState, + batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanInCollection inScope(final String scope) { + return new ReactiveRangeScanSupport<>(template, domainType, scope != null ? scope : this.scope, collection, + options, sort, mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanInScope withSort(Object sort) { + return new ReactiveRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanWithSort consistentWith(MutationState mutationState) { + return new ReactiveRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanConsistentWith as(Class returnType) { + return new ReactiveRangeScanSupport<>(template, returnType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanWithProjection withBatchItemLimit(Integer batchItemLimit) { + return new ReactiveRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public RangeScanWithBatchItemLimit withBatchByteLimit(Integer batchByteLimit) { + return new ReactiveRangeScanSupport<>(template, domainType, scope, collection, options, sort, + mutationState, batchItemLimit, batchByteLimit, support); + } + + @Override + public Flux rangeScan(String lower, String upper) { + return rangeScan(lower, upper, false, null, null); + } + + @Override + public Flux sampleScan(Long limit, Long... seed) { + return rangeScan(null, null, true, limit, seed!= null && seed.length > 0 ? seed[0] : null); + } + + + Flux rangeScan(String lower, String upper, boolean isSamplingScan, Long limit, Long seed) { + + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("rangeScan lower={} upper={} {}", lower, upper, pArgs); + } + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive(); + + ScanType scanType = null; + if(isSamplingScan){ + scanType = ScanType.samplingScan(limit, seed != null ? seed : 0); + } else { + ScanTerm lowerTerm = null; + ScanTerm upperTerm = null; + if (lower != null) { + lowerTerm = ScanTerm.inclusive(lower); + } + if (upper != null) { + upperTerm = ScanTerm.inclusive(upper); + } + scanType = ScanType.rangeScan(lowerTerm, upperTerm); + } + + Flux reactiveEntities = TransactionalSupport.verifyNotInTransaction("rangeScan") + .thenMany(rc.scan(scanType, buildScanOptions(pArgs.getOptions(), false)) + .flatMap(result -> support.decodeEntity(result.id(), + new String(result.contentAsBytes(), StandardCharsets.UTF_8), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null, null))); + + return reactiveEntities.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + + } + + @Override + public Flux rangeScanIds(String upper, String lower) { + return rangeScanIds(upper, lower, false, null, null); + } + + @Override + public Flux sampleScanIds(Long limit, Long... seed) { + return rangeScanIds(null, null, true, limit, seed!= null && seed.length > 0 ? seed[0] : null); + } + + Flux rangeScanIds(String lower, String upper, boolean isSamplingScan, Long limit, Long seed) { + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("rangeScan lower={} upper={} {}", lower, upper, pArgs); + } + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive(); + + ScanType scanType = null; + if(isSamplingScan){ + scanType = ScanType.samplingScan(limit, seed != null ? seed : 0); + } else { + ScanTerm lowerTerm = null; + ScanTerm upperTerm = null; + if (lower != null) { + lowerTerm = ScanTerm.inclusive(lower); + } + if (upper != null) { + upperTerm = ScanTerm.inclusive(upper); + } + scanType = ScanType.rangeScan(lowerTerm, upperTerm); + } + + Flux reactiveEntities = TransactionalSupport.verifyNotInTransaction("rangeScanIds") + .thenMany(rc.scan(scanType, buildScanOptions(pArgs.getOptions(), true)).map(result -> result.id())); + + return reactiveEntities.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + + } + + private ScanOptions buildScanOptions(ScanOptions options, Boolean idsOnly) { + return OptionsBuilder.buildScanOptions(options, sort, idsOnly, mutationState, batchByteLimit, batchItemLimit); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index 8fb19d5b6..bbae989e2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 2e7427f27..ff8f8613c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -67,7 +67,7 @@ public ReactiveRemoveById removeById() { public ReactiveRemoveById removeById(Class domainType) { return new ReactiveRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null); } @@ -157,7 +157,7 @@ private void rejectInvalidTransactionalOptions() { public Mono oneEntity(Object entity) { ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, template.support().getCas(entity)); - return op.one(template.support().getId(entity).toString()); + return op.withCas(template.support().getCas(entity)).one(template.support().getId(entity).toString()); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 7a27785fb..b91097f69 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index 205fc9c2d..cc9b3c50e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.api.query.CoreQueryContext; +import com.couchbase.client.core.io.CollectionIdentifier; import reactor.core.publisher.Flux; import java.util.Optional; @@ -28,7 +30,6 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; -import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode; import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.query.QueryOptions; @@ -100,11 +101,10 @@ public Flux all() { } else { TransactionQueryOptions opts = OptionsBuilder .buildTransactionQueryOptions(buildQueryOptions(pArgs.getOptions())); - ObjectNode convertedOptions = com.couchbase.client.java.transactions.internal.OptionsUtil - .createTransactionOptions(pArgs.getScope() == null ? null : rs, statement, opts); + CoreQueryContext queryContext = OptionsBuilder.queryContext(pArgs.getScope(), pArgs.getCollection(), rs.bucketName()); return transactionContext.get().getCore() - .queryBlocking(statement, template.getBucketName(), pArgs.getScope(), convertedOptions, false) - .flatMapIterable(result -> result.rows).map(row -> { + .queryBlocking(statement, queryContext, opts.builder().build(), false) + .flatMapIterable(result -> result.collectRows()).map(row -> { JsonObject json = JsonObject.fromJson(row.data()); return new RemoveResult(json.getString(TemplateUtils.SELECT_ID), json.getLong(TemplateUtils.SELECT_CAS), Optional.empty()); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index a651a9804..fd4a27a9f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 3c2ae98c3..05392598a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -64,7 +64,7 @@ public ReactiveReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 5ad4c0b04..24be1c749 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index 77c127a45..8b8d0a72c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 01ec65eb4..aa5bf920d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -53,7 +53,7 @@ public ReactiveUpsertById upsertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), - OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/RemoveResult.java b/src/main/java/org/springframework/data/couchbase/core/RemoveResult.java index 7be1bf429..84620fb94 100644 --- a/src/main/java/org/springframework/data/couchbase/core/RemoveResult.java +++ b/src/main/java/org/springframework/data/couchbase/core/RemoveResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 5422e6904..935392ab4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java index e3814ba38..5661ba7b5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/UnsupportedCouchbaseFeatureException.java b/src/main/java/org/springframework/data/couchbase/core/UnsupportedCouchbaseFeatureException.java index 0936b2221..41d2cd4f8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/UnsupportedCouchbaseFeatureException.java +++ b/src/main/java/org/springframework/data/couchbase/core/UnsupportedCouchbaseFeatureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java index ff35edc59..6f2c8f738 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -29,7 +29,6 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; /** @@ -38,6 +37,7 @@ * @author Michael Nitschinger * @author Mark Paluch * @author Michael Reiche + * @author Vipul Gupta */ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean { @@ -54,15 +54,17 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, /** * Holds the custom conversions. */ - protected CustomConversions conversions = new CouchbaseCustomConversions(Collections.emptyList()); + protected CustomConversions conversions; /** - * Create a new converter and hand it over the {@link ConversionService} + * Create a new converter with custom conversions and hand it over the {@link ConversionService} * * @param conversionService the conversion service to use. + * @param customConversions the custom conversions to use */ - protected AbstractCouchbaseConverter(final GenericConversionService conversionService) { + protected AbstractCouchbaseConverter(final GenericConversionService conversionService, final CustomConversions customConversions) { this.conversionService = conversionService; + this.conversions = customConversions; } /** @@ -163,12 +165,12 @@ public Object convertForWriteIfNeeded(Object inValue) { // superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : // value; } else if (value instanceof Collection || elementType.isArray()) { - TypeInformation type = ClassTypeInformation.from(value.getClass()); + TypeInformation type = TypeInformation.of(value.getClass()); value = ((MappingCouchbaseConverter) this).writeCollectionInternal(MappingCouchbaseConverter.asCollection(value), new CouchbaseList(conversions.getSimpleTypeHolder()), type, null, null); } else { CouchbaseDocument embeddedDoc = new CouchbaseDocument(); - TypeInformation type = ClassTypeInformation.from(value.getClass()); + TypeInformation type = TypeInformation.of(value.getClass()); ((MappingCouchbaseConverter) this).writeInternalRoot(value, embeddedDoc, type, false, null, true); value = embeddedDoc; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java index 077df4b59..b02241b92 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java @@ -1,6 +1,6 @@ package org.springframework.data.couchbase.core.convert; /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -20,7 +20,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.data.convert.ReadingConverter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.couchbase.client.core.encryption.CryptoManager; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java index ae0dda9d2..af2c2a01a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java index 7dd7d70b6..46edc64df 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConversionContext.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConversionContext.java index f52d22fb5..fcc3a270b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConversionContext.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConversionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -19,7 +19,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link CouchbaseConverter}. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java index b16d526c7..748c02734 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java index 76094992a..75303d214 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -31,6 +31,7 @@ import java.util.Set; import java.util.function.Consumer; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; @@ -39,7 +40,9 @@ import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterRegistrar; import org.springframework.data.convert.SimplePropertyValueConversions; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.CouchbaseSimpleTypes; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.util.Assert; @@ -54,7 +57,8 @@ * @author Oliver Gierke * @author Mark Paluch * @author Subhashni Balakrishnan - * @Michael Reiche + * @author Michael Reiche + * @author Tigran Babloyan * @see org.springframework.data.convert.CustomConversions * @see SimpleTypeHolder * @since 2.0 @@ -74,7 +78,7 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert converters.addAll(OtherConverters.getConvertersToRegister()); STORE_CONVERTERS = Collections.unmodifiableList(converters); - STORE_CONVERSIONS = StoreConversions.of(SimpleTypeHolder.DEFAULT, STORE_CONVERTERS); + STORE_CONVERSIONS = StoreConversions.of(CouchbaseSimpleTypes.DOCUMENT_TYPES, STORE_CONVERTERS); } /** @@ -143,6 +147,41 @@ public static CouchbaseConverterConfigurationAdapter from(List converters) { CouchbaseConverterConfigurationAdapter converterConfigurationAdapter = new CouchbaseConverterConfigurationAdapter(); converterConfigurationAdapter.registerConverters(converters); + // The following + ObjectMapper om = new AbstractCouchbaseConfiguration() { + @Override + public String getConnectionString() { + return null; + } + + @Override + public String getUserName() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getBucketName() { + return null; + } + }.getObjectMapper(); + List newConverters = new ArrayList(); + newConverters.add(new OtherConverters.EnumToObject(om)); + newConverters.add(new IntegerToEnumConverterFactory(om)); + newConverters.add(new StringToEnumConverterFactory(om)); + newConverters.add(new BooleanToEnumConverterFactory(om)); + SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions(); + valueConversions.setConverterFactory( + new CouchbasePropertyValueConverterFactory(null, AbstractCouchbaseConfiguration.annotationToConverterMap(), om)); + valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry()); + valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory + converterConfigurationAdapter.setPropertyValueConversions(valueConversions); + converterConfigurationAdapter.registerConverters(newConverters); + converterConfigurationAdapter.registerConverters(newConverters); return converterConfigurationAdapter; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java index 95ef2bca9..484420eab 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseJsr310Converters.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseJsr310Converters.java index ec4601f6e..219f1e220 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseJsr310Converters.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseJsr310Converters.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java index 3122ff87f..83d29b260 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java index 03c7b953b..0c197667c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java index 71c9e56f5..aba239ad5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java index 330755e59..9493fad35 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java index 49f39f258..dd2f47871 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java b/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java index 57c872665..860d79c40 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java index 2be32ec87..58f284ac1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -67,6 +67,9 @@ public CouchbaseDocumentTypeAliasAccessor(final String typeKey) { @Override public Alias readAliasFrom(final CouchbaseDocument source) { + if (typeKey == null || typeKey.length() == 0) { + return Alias.NONE; + } return Alias.ofNullable(source.get(typeKey)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java index 8eae869f1..984e01888 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -20,7 +20,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.data.convert.ReadingConverter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.couchbase.client.core.encryption.CryptoManager; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java index 2a8a13490..0eb576e32 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index e381b7382..0e1d79c1c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,10 +16,10 @@ package org.springframework.data.couchbase.core.convert; -import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.UNIQUE; -import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.USE_ATTRIBUTES; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.*; import java.beans.Transient; +import java.lang.reflect.InaccessibleObjectException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -30,12 +30,17 @@ import java.util.TreeMap; import java.util.UUID; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; @@ -60,18 +65,18 @@ import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.EntityInstantiator; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; -import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -89,8 +94,9 @@ * @author Mark Paluch * @author Michael Reiche * @author Remi Bleuse + * @author Vipul Gupta */ -public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implements ApplicationContextAware { +public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implements ApplicationContextAware, EnvironmentCapable, EnvironmentAware { /** * The default "type key", the name of the field that will hold type information. @@ -111,6 +117,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * Spring Expression Language context. */ private final SpELContext spELContext; + + private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + + private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory; /** * The overall application context. */ @@ -125,6 +135,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem */ private @Nullable EntityCallbacks entityCallbacks; + private @Nullable Environment environment; + public MappingCouchbaseConverter() { this(new CouchbaseMappingContext(), null); } @@ -139,28 +151,41 @@ public MappingCouchbaseConverter( this(mappingContext, null); } + /** + * Create a new {@link MappingCouchbaseConverter} + * + * @param mappingContext the mapping context to use. + * @param typeKey the attribute name to use to store complex types class name. + */ + public MappingCouchbaseConverter( + final MappingContext, CouchbasePersistentProperty> mappingContext, + final String typeKey) { + this(mappingContext, typeKey, new CouchbaseCustomConversions(Collections.emptyList())); + } + /** * Create a new {@link MappingCouchbaseConverter} that will store class name for complex types in the typeKey * attribute. * * @param mappingContext the mapping context to use. * @param typeKey the attribute name to use to store complex types class name. + * @param customConversions the custom conversions to use */ public MappingCouchbaseConverter( final MappingContext, CouchbasePersistentProperty> mappingContext, - final String typeKey) { - super(new DefaultConversionService()); + final String typeKey, + final CustomConversions customConversions) { + super(new DefaultConversionService(), customConversions); this.mappingContext = mappingContext; - // this is how the MappingCouchbaseConverter gets the custom conversions. - // the conversions Service gets them in afterPropertiesSet() - CustomConversions customConversions = new CouchbaseCustomConversions(Collections.emptyList()); - this.setCustomConversions(customConversions); // Don't rely on setSimpleTypeHolder being called in afterPropertiesSet() - some integration tests do not use it // if the mappingContext does not have the SimpleTypes, it will not know that they have converters, then it will // try to access the fields of the type and (maybe) fail with InaccessibleObjectException ((CouchbaseMappingContext) mappingContext).setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); typeMapper = new DefaultCouchbaseTypeMapper(typeKey != null ? typeKey : TYPEKEY_DEFAULT); spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE); + + expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory( + expressionParser, this, o -> spELContext.getEvaluationContext(o)); } /** @@ -188,6 +213,21 @@ private static boolean isSubtype(final Class left, final Class right) { return left.isAssignableFrom(right) && !left.equals(right); } + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public Environment getEnvironment() { + + if (this.environment == null) { + this.environment = new StandardEnvironment(); + } + + return environment; + } + @Override public MappingContext, CouchbasePersistentProperty> getMappingContext() { return mappingContext; @@ -205,7 +245,7 @@ public Alias getTypeAlias(TypeInformation info) { @Override public R read(final Class clazz, final CouchbaseDocument source) { - return read(ClassTypeInformation.from(clazz), source, null); + return read(TypeInformation.of(clazz), source, null); } /** @@ -261,7 +301,8 @@ protected R read(final TypeInformation type, final CouchbaseDocument sour * @return the converted entity. */ protected R read(final CouchbasePersistentEntity entity, final CouchbaseDocument source, final Object parent) { - final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); + + ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(source); ParameterValueProvider provider = getParameterProvider(entity, source, evaluator, parent); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); @@ -272,7 +313,7 @@ protected R read(final CouchbasePersistentEntity entity, final CouchbaseD entity.doWithProperties(new PropertyHandler<>() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { - if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop) + if (!doesPropertyExistInSource(prop) || entity.isCreatorArgument(prop) || isIdConstructionProperty(prop) || prop.isAnnotationPresent(N1qlJoin.class)) { return; } @@ -318,7 +359,7 @@ private boolean isIdConstructionProperty(final CouchbasePersistentProperty prope */ protected Object getValueInternal(final CouchbasePersistentProperty property, final CouchbaseDocument source, final Object parent, PersistentEntity entity) { - return new CouchbasePropertyValueProvider(source, spELContext, parent, entity).getPropertyValue(property); + return new CouchbasePropertyValueProvider(source, expressionEvaluatorFactory.create(source), parent, entity).getPropertyValue(property); } /** @@ -332,7 +373,7 @@ protected Object getValueInternal(final CouchbasePersistentProperty property, fi */ private ParameterValueProvider getParameterProvider( final CouchbasePersistentEntity entity, final CouchbaseDocument source, - final DefaultSpELExpressionEvaluator evaluator, final Object parent) { + final ValueExpressionEvaluator evaluator, final Object parent) { CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent, entity); PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( entity, provider, parent); @@ -437,7 +478,7 @@ public void write(final Object source, final CouchbaseDocument target) { } boolean isCustom = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class).isPresent(); - TypeInformation type = ClassTypeInformation.from(source.getClass()); + TypeInformation type = TypeInformation.of(source.getClass()); if (!isCustom) { typeMapper.writeType(type, target); @@ -471,7 +512,7 @@ public void writeInternalRoot(final Object source, CouchbaseDocument target, Typ } if (Map.class.isAssignableFrom(source.getClass())) { - writeMapInternal((Map) source, target, ClassTypeInformation.MAP, property); + writeMapInternal((Map) source, target, TypeInformation.MAP, property); return; } @@ -613,7 +654,7 @@ public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { return; } - if (!conversions.isSimpleType(prop.getType())) { + if (!conversions.isSimpleType(propertyObj.getClass())) { writePropertyInternal(propertyObj, target, prop, accessor); } else { writeSimpleInternal(prop, accessor, target, prop.getFieldName()); @@ -638,7 +679,7 @@ protected void writePropertyInternal(final Object source, final CouchbaseDocumen } String name = prop.getFieldName(); - TypeInformation valueType = ClassTypeInformation.from(source.getClass()); + TypeInformation valueType = TypeInformation.of(source.getClass()); TypeInformation type = prop.getTypeInformation(); if (valueType.isCollectionLike()) { CouchbaseList collectionDoc = createCollection(asCollection(source), valueType, prop, accessor); @@ -812,7 +853,7 @@ private Object readCollection(final TypeInformation targetType, final Couchba if (dbObjItem instanceof CouchbaseDocument) { items.add(read(componentType, (CouchbaseDocument) dbObjItem, parent)); } else if (dbObjItem instanceof CouchbaseList) { - items.add(readCollection(componentType, (CouchbaseList) dbObjItem, parent)); + items.add(readCollection(componentType != null ? componentType :TypeInformation.of(dbObjItem.getClass()), (CouchbaseList) dbObjItem, parent)); } else { items.add(getPotentiallyConvertedSimpleRead(dbObjItem, rawComponentType)); } @@ -902,6 +943,10 @@ public void setApplicationContext(ApplicationContext applicationContext) { if (entityCallbacks == null) { setEntityCallbacks(EntityCallbacks.create(applicationContext)); } + ClassLoader classLoader = applicationContext.getClassLoader(); + if (this.typeMapper instanceof BeanClassLoaderAware && classLoader != null) { + ((BeanClassLoaderAware) this.typeMapper).setBeanClassLoader(classLoader); + } } /** @@ -963,7 +1008,7 @@ public R readValue(Object value, CouchbasePersistentProperty prop, Object pa } } if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { - TypeInformation ti = ClassTypeInformation.from(value.getClass()); + TypeInformation ti = TypeInformation.of(value.getClass()); return (R) conversionService.convert(value, ti.toTypeDescriptor(), new TypeDescriptor(prop.getField())); } if (value instanceof CouchbaseDocument) { @@ -978,7 +1023,16 @@ public R readValue(Object value, CouchbasePersistentProperty prop, Object pa private ConvertingPropertyAccessor getPropertyAccessor(Object source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); + CouchbasePersistentEntity entity = null; + try { + entity = mappingContext.getRequiredPersistentEntity(source.getClass()); + } catch(InaccessibleObjectException e){ + try { // punt + entity = mappingContext.getRequiredPersistentEntity(Object.class); + } catch(Exception ee){ + throw e; + } + } PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); return new ConvertingPropertyAccessor<>(accessor, conversionService); @@ -1045,7 +1099,7 @@ private class CouchbasePropertyValueProvider implements PropertyValueProvider R getPropertyValue(final CouchbasePersistentProperty property) { } } - String maybeMangle(PersistentProperty property) { + public String maybeMangle(PersistentProperty property) { Assert.notNull(property, "property"); if (!conversions.hasValueConverter(property)) { return ((CouchbasePersistentProperty) property).getFieldName(); @@ -1123,11 +1172,11 @@ String maybeMangle(PersistentProperty property) { * A expression parameter value provider. */ private class ConverterAwareSpELExpressionParameterValueProvider - extends SpELExpressionParameterValueProvider { + extends ValueExpressionParameterValueProvider { private final Object parent; - public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator, + public ConverterAwareSpELExpressionParameterValueProvider(final ValueExpressionEvaluator evaluator, final ConversionService conversionService, final ParameterValueProvider delegate, final Object parent) { super(evaluator, conversionService, delegate); @@ -1135,8 +1184,7 @@ public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEv } @Override - protected T potentiallyConvertSpelValue(final Object object, - final Parameter parameter) { + protected T potentiallyConvertExpressionValue(Object object, Parameter parameter) { return readValue(object, parameter.getType(), parent); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java index 5914dd018..7392ecfd3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -22,19 +22,29 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.time.YearMonth; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; -import org.springframework.util.Base64Utils; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseList; import com.couchbase.client.core.encryption.CryptoManager; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValueModule; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -52,13 +62,11 @@ private OtherConverters() {} * @return the list of converters to register. */ public static Collection> getConvertersToRegister() { - List> converters = new ArrayList>(); + List> converters = new ArrayList<>(); converters.add(UuidToString.INSTANCE); converters.add(StringToUuid.INSTANCE); - converters.add(BigIntegerToString.INSTANCE); converters.add(StringToBigInteger.INSTANCE); - converters.add(BigDecimalToString.INSTANCE); converters.add(StringToBigDecimal.INSTANCE); converters.add(ByteArrayToString.INSTANCE); converters.add(StringToByteArray.INSTANCE); @@ -66,6 +74,14 @@ private OtherConverters() {} converters.add(StringToCharArray.INSTANCE); converters.add(ClassToString.INSTANCE); converters.add(StringToClass.INSTANCE); + converters.add(MapToJsonNode.INSTANCE); + converters.add(JsonNodeToMap.INSTANCE); + converters.add(JsonObjectToMap.INSTANCE); + converters.add(MapToJsonObject.INSTANCE); + converters.add(JsonArrayToCouchbaseList.INSTANCE); + converters.add(CouchbaseListToJsonArray.INSTANCE); + converters.add(YearMonthToStringConverter.INSTANCE); + converters.add(StringToYearMonthConverter.INSTANCE); // EnumToObject, IntegerToEnumConverterFactory and StringToEnumConverterFactory are // registered in // {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( @@ -93,16 +109,7 @@ public UUID convert(String source) { } } - @WritingConverter - public enum BigIntegerToString implements Converter { - INSTANCE; - - @Override - public String convert(BigInteger source) { - return source == null ? null : source.toString(); - } - } - + // to support reading BigIntegers that were written as Strings (now discontinued) @ReadingConverter public enum StringToBigInteger implements Converter { INSTANCE; @@ -113,16 +120,7 @@ public BigInteger convert(String source) { } } - @WritingConverter - public enum BigDecimalToString implements Converter { - INSTANCE; - - @Override - public String convert(BigDecimal source) { - return source == null ? null : source.toString(); - } - } - + // to support reading BigDecimals that were written as Strings (now discontinued) @ReadingConverter public enum StringToBigDecimal implements Converter { INSTANCE; @@ -139,7 +137,7 @@ public enum ByteArrayToString implements Converter { @Override public String convert(byte[] source) { - return source == null ? null : Base64Utils.encodeToString(source); + return source == null ? null : Base64.getEncoder().encodeToString(source); } } @@ -149,7 +147,7 @@ public enum StringToByteArray implements Converter { @Override public byte[] convert(String source) { - return source == null ? null : Base64Utils.decode(source.getBytes(StandardCharsets.UTF_8)); + return source == null ? null : Base64.getDecoder().decode(source.getBytes(StandardCharsets.UTF_8)); } } @@ -231,4 +229,107 @@ public Object convert(Enum source) { } } + @WritingConverter + public enum JsonNodeToMap implements Converter { + INSTANCE; + static ObjectMapper mapper= new ObjectMapper().registerModule(new JsonValueModule()); + @Override + public CouchbaseDocument convert(JsonNode source) { + if( source == null ){ + return null; + } + return new CouchbaseDocument().setContent((Map)mapper.convertValue(source, new TypeReference>(){})); + } + } + + @ReadingConverter + public enum MapToJsonNode implements Converter { + INSTANCE; + static ObjectMapper mapper= new ObjectMapper().registerModule(new JsonValueModule()); + + @Override + public JsonNode convert(CouchbaseDocument source) { + if( source == null ){ + return null; + } + return mapper.valueToTree(source.export()); + } + } + + @WritingConverter + public enum JsonObjectToMap implements Converter { + INSTANCE; + + @Override + public CouchbaseDocument convert(JsonObject source) { + if( source == null ){ + return null; + } + return new CouchbaseDocument().setContent(source); + } + } + + @ReadingConverter + public enum MapToJsonObject implements Converter { + INSTANCE; + static ObjectMapper mapper= new ObjectMapper(); + + @Override + public JsonObject convert(CouchbaseDocument source) { + if( source == null ){ + return null; + } + return JsonObject.from(source.export()); + } + } + + @WritingConverter + public enum JsonArrayToCouchbaseList implements Converter { + INSTANCE; + + @Override + public CouchbaseList convert(JsonArray source) { + if( source == null ){ + return null; + } + return new CouchbaseList(source.toList()); + } + } + + @ReadingConverter + public enum CouchbaseListToJsonArray implements Converter { + INSTANCE; + static ObjectMapper mapper= new ObjectMapper(); + + @Override + public JsonArray convert(CouchbaseList source) { + if( source == null ){ + return null; + } + return JsonArray.from(source.export()); + } + } + + @WritingConverter + public enum YearMonthToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(YearMonth source) { + return Optional.ofNullable(source).map(YearMonth::toString).orElse(null); + } + } + + @ReadingConverter + public enum StringToYearMonthConverter implements Converter { + + INSTANCE; + + @Override + public YearMonth convert(String source) { + return Optional.ofNullable(source).map(YearMonth::parse).orElse(null); + } + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java index b1b6a90cd..adccb4d0d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -18,7 +18,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.data.convert.ReadingConverter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.couchbase.client.core.encryption.CryptoManager; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/TypeAwareTypeInformationMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/TypeAwareTypeInformationMapper.java index f17e460b8..1496d1a1e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/TypeAwareTypeInformationMapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/TypeAwareTypeInformationMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java index 065eaf774..df465939d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors + * Copyright 2018-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java index f50bc168d..597d5018f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -220,7 +220,7 @@ private Object decodePrimitive(final JsonToken token, final JsonParser parser) t case VALUE_NUMBER_INT: return parser.getNumberValue(); case VALUE_NUMBER_FLOAT: - return parser.getDoubleValue(); + return parser.getDecimalValue(); case VALUE_NULL: return null; default: diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java index 8468c86a0..d3aa9433d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java index 80bcc238e..54190a6d4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java index 1de36a62d..d8174e67d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java index 181651ed5..3586b9a6e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java index 2875b6172..eb058973d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -15,6 +15,9 @@ */ package org.springframework.data.couchbase.core.index; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.s; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -25,10 +28,11 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.mapping.Alias; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -142,7 +146,15 @@ protected List createCompositeQueryIndexDefinitions(final private String getPredicate(final MappingCouchbaseEntityInformation entityInfo) { String typeKey = operations.getConverter().getTypeKey(); String typeValue = entityInfo.getJavaType().getName(); - return "`" + typeKey + "` = \"" + typeValue + "\""; + Alias alias = operations.getConverter().getTypeAlias(TypeInformation.of(entityInfo.getJavaType())); + if (alias != null && alias.isPresent()) { + typeValue = alias.toString(); + } + return !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)).toString() : null; + } + + private static boolean empty(String s){ + return s == null || s.length() == 0; } public static class IndexDefinitionHolder implements IndexDefinition { diff --git a/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java b/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java index fe3ccff58..3239d2a7b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the original author or authors. + * Copyright 2011-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java index 27383ce54..f06237591 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java index fad6d6efc..d14a395c3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. @@ -20,7 +20,6 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -66,7 +65,7 @@ static QueryIndexResolver create( * @see 2.2 */ default Iterable resolveIndexFor(Class entityType) { - return resolveIndexFor(ClassTypeInformation.from(entityType)); + return resolveIndexFor(TypeInformation.of(entityType)); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java index 36620976c..5e90b4294 100644 --- a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index 729aeaebb..9a210a92d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -21,6 +21,7 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.env.Environment; @@ -36,6 +37,7 @@ * @author Michael Nitschinger * @author Mark Paluch * @author Michael Reiche + * @author Tigran Babloyan */ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity implements CouchbasePersistentEntity, EnvironmentAware { @@ -50,6 +52,7 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity typeInformation) { super(typeInformation); validateExpirationConfiguration(); + validateDurabilityConfiguration(); } private void validateExpirationConfiguration() { @@ -61,6 +64,15 @@ private void validateExpirationConfiguration() { } } + private void validateDurabilityConfiguration() { + Document annotation = getType().getAnnotation(Document.class); + if (annotation != null && annotation.durabilityLevel() != DurabilityLevel.NONE && StringUtils.hasLength(annotation.durabilityExpression())) { + String msg = String.format("Incorrect durability configuration on class %s using %s. " + + "You cannot use 'durabilityLevel' and 'durabilityExpression' at the same time", getType().getName(), annotation); + throw new IllegalArgumentException(msg); + } + } + @Override public void setEnvironment(Environment environment) { this.environment = environment; @@ -158,6 +170,30 @@ private static int getExpiryValue(Expiry annotation, Environment environment) { return expiryValue; } + @Override + public DurabilityLevel getDurabilityLevel() { + return getDurabilityLevel(AnnotatedElementUtils.findMergedAnnotation(getType(), Durability.class), environment); + } + + private static DurabilityLevel getDurabilityLevel(Durability annotation, Environment environment) { + if (annotation == null) { + return DurabilityLevel.NONE; + } + DurabilityLevel durabilityLevel = annotation.durabilityLevel(); + String durabilityExpressionString = annotation.durabilityExpression(); + if (StringUtils.hasLength(durabilityExpressionString)) { + Assert.notNull(environment, "Environment must be set to use 'durabilityExpressionString'"); + String durabilityWithReplacedPlaceholders = environment.resolveRequiredPlaceholders(durabilityExpressionString); + try { + durabilityLevel = DurabilityLevel.valueOf(durabilityWithReplacedPlaceholders); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid value for durability expression: " + durabilityWithReplacedPlaceholders); + } + } + return durabilityLevel; + } + @Override public boolean isTouchOnRead() { org.springframework.data.couchbase.core.mapping.Document annotation = getType() diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java index ddd8e63b9..b6213cbcf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java index 8212ab88a..49428c503 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java index edcd9e6ea..7b054edfc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java index 5f4cfc3f6..2d6b9a03a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index 9c7357b0b..3a9b5aa0d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -18,6 +18,7 @@ import java.time.Duration; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import org.springframework.data.mapping.PersistentEntity; /** @@ -25,6 +26,7 @@ * * @author Michael Nitschinger * @author Michael Reiche + * @author Tigran Babloyan */ public interface CouchbasePersistentEntity extends PersistentEntity { @@ -54,6 +56,15 @@ public interface CouchbasePersistentEntity extends PersistentEntity + * Allows the application to wait until this replication (or persistence) is successful before proceeding + * + * @return the durability level. + */ + DurabilityLevel getDurabilityLevel(); + /** * Flag for using getAndTouch operations for reads, resetting the expiration (if one was set) when the entity is * directly read (eg. findOne, findById). diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java index 3fa7529b2..e42878c43 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java index 3811446c6..a40d71dc0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -38,7 +38,7 @@ public abstract class CouchbaseSimpleTypes { Stream.of(JsonObject.class, JsonArray.class, Number.class).collect(toSet()), true); public static final SimpleTypeHolder DOCUMENT_TYPES = new SimpleTypeHolder( - Stream.of(CouchbaseDocument.class, CouchbaseList.class).collect(toSet()), true); + Stream.of(CouchbaseDocument.class, CouchbaseList.class, Number.class).collect(toSet()), true); private CouchbaseSimpleTypes() {} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java index e5c87b7bc..2090409df 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java index a2caeec55..ef8c61bf4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -47,6 +47,7 @@ @Target({ ElementType.TYPE }) @Expiry @ScanConsistency +@Durability public @interface Document { /** @@ -105,5 +106,16 @@ * The optional durabilityLevel for all mutating operations, allows the application to wait until this replication * (or persistence) is successful before proceeding */ + @AliasFor(annotation = Durability.class, attribute = "durabilityLevel") DurabilityLevel durabilityLevel() default DurabilityLevel.NONE; + + /** + * Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism. + * Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}.
+ * Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}. + *
+ * SpEL is NOT supported. + */ + @AliasFor(annotation = Durability.class, attribute = "durabilityExpression") + String durabilityExpression() default ""; } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Durability.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Durability.java new file mode 100644 index 000000000..d9eba776a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Durability.java @@ -0,0 +1,32 @@ +package org.springframework.data.couchbase.core.mapping; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import org.springframework.data.annotation.Persistent; + +import java.lang.annotation.*; + +/** + * Durability annotation + * + * @author Tigran Babloyan + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +public @interface Durability { + /** + * The optional durabilityLevel for all mutating operations, allows the application to wait until this replication + * (or persistence) is successful before proceeding + */ + DurabilityLevel durabilityLevel() default DurabilityLevel.NONE; + + /** + * Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism. + * Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}.
+ * Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}. + *
+ * SpEL is NOT supported. + */ + String durabilityExpression() default ""; +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiration.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiration.java index af646fa34..334de2967 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiration.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiration.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java index 663399df3..043f70fe7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Field.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Field.java index 673af0018..99b17f63b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Field.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Field.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java b/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java index 4bc6db564..f819375e7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java index 8e5ef7081..93546f7b0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java index 112221d2d..6e255742c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterDeleteEvent.java index 94bd23aed..6ccce2ea1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterDeleteEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterDeleteEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveEvent.java index 45c54f9e5..597ec6aee 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java index 101d4fbd6..c864aef20 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java index 01e76d9b6..43cebf403 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -35,9 +35,9 @@ * @author Mark Paluch * @author Michael Reiche */ -public class AuditingEventListener implements ApplicationListener> { +public class AuditingEventListener implements ApplicationListener> { - private final ObjectFactory auditingHandlerFactory; + private final ObjectFactory auditingHandlerFactory; public AuditingEventListener() { this.auditingHandlerFactory = null; @@ -51,20 +51,37 @@ public AuditingEventListener() { * * @param auditingHandlerFactory must not be {@literal null}. */ - public AuditingEventListener(ObjectFactory auditingHandlerFactory) { - Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + public AuditingEventListener(ObjectFactory auditingHandlerFactory) { + Assert.notNull(auditingHandlerFactory, "auditingHandlerFactory must not be null!"); this.auditingHandlerFactory = auditingHandlerFactory; + Object o = auditingHandlerFactory.getObject(); + if(!(o instanceof IsNewAwareAuditingHandler)){ + LOG.warn("auditingHandler IS NOT an IsNewAwareAuditingHandler: {}",o); + } else { + LOG.info("auditingHandler IS an IsNewAwareAuditingHandler: {}",o); + } } - /* + /** * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + * @see {@link org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)} */ @Override - public void onApplicationEvent(CouchbaseMappingEvent event) { + public void onApplicationEvent(CouchbaseMappingEvent event) { if (event instanceof BeforeConvertEvent) { - Optional.ofNullable(event.getSource())// - .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); + IsNewAwareAuditingHandler h = auditingHandlerFactory != null + && auditingHandlerFactory.getObject() instanceof IsNewAwareAuditingHandler + ? (IsNewAwareAuditingHandler) (auditingHandlerFactory.getObject()) + : null; + if (auditingHandlerFactory != null && h == null) { + if (LOG.isWarnEnabled()) { + LOG.warn("event:{} source:{} auditingHandler is not an IsNewAwareAuditingHandler: {}", + event.getClass().getSimpleName(), event.getSource(), auditingHandlerFactory.getObject()); + } + } + if (h != null) { + Optional.ofNullable(event.getSource()).ifPresent(it -> h.markAudited(it)); + } } if (event instanceof BeforeSaveEvent) {} if (event instanceof AfterSaveEvent) {} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java index 558771d21..1d2404cf3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertEvent.java index a07247ea7..52f237311 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeDeleteEvent.java index 29a983bba..3a6440957 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeDeleteEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeDeleteEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveEvent.java index ab3434dde..daa7c3f59 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/CouchbaseMappingEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/CouchbaseMappingEvent.java index 2feaa7df8..829d6803e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/CouchbaseMappingEvent.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/CouchbaseMappingEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/LoggingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/LoggingEventListener.java index 0661c0552..d8c71a2cf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/LoggingEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/LoggingEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java index bcd529c81..686bb47bd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java index b200aadde..56ace58e9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java index fd28dac77..a819d2942 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java index 1edb7a668..14f5cd2e2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java index 7b8ebf2a5..049ddd8b5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java index dca019852..4e11108e8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java index cd273b680..a031b88d3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java index 5ba5f8e03..0d3290bc7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java index ad53f8280..ac9494136 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/AnalyticsQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/AnalyticsQuery.java index 0f2567577..2254580f9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/AnalyticsQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/AnalyticsQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java b/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java index e9a5f1ed9..0303c9ab5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java b/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java index 7484a0d57..5c6eadc9b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/FetchType.java b/src/main/java/org/springframework/data/couchbase/core/query/FetchType.java index f120afa3c..8dbf02c43 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/FetchType.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/FetchType.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors + * Copyright 2018-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/HashSide.java b/src/main/java/org/springframework/data/couchbase/core/query/HashSide.java index 3ad5b7f70..83ca7c6c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/HashSide.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/HashSide.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors + * Copyright 2018-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Meta.java b/src/main/java/org/springframework/data/couchbase/core/query/Meta.java index d7ad15893..b2d6d2bfa 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Meta.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Meta.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -20,7 +20,7 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java b/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java index da69b5622..a27d967d4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1QLExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java index 93858255a..9d0816de4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -41,12 +41,18 @@ public QueryOptions getOptions() { return options; } + // for logging only public JsonObject n1ql() { JsonObject query = JsonObject.create().put("statement", expression.toString()); - options.build().injectParams(query); + query.put("options", OptionsBuilder.getQueryOpts(options.build())); return query; } + @Override + public boolean isReadonly() { + return options.build().readonly(); + } + @Override public String toN1qlSelectString(CouchbaseConverter template, String bucketName, String scopeName, String collectionName, Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1qlJoin.java b/src/main/java/org/springframework/data/couchbase/core/query/N1qlJoin.java index ad3abecec..1d3163077 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1qlJoin.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1qlJoin.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors + * Copyright 2018-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java index 9d850aeca..746ff2016 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java index 1c42702c1..2e9b8ae9a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java index c3a0e2cf0..775605c53 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. @@ -27,16 +27,22 @@ import java.util.Map; import java.util.Optional; +import com.couchbase.client.core.api.query.CoreQueryContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.couchbase.repository.Scope; import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; +import com.couchbase.client.core.api.query.CoreQueryScanConsistency; +import com.couchbase.client.core.classic.query.ClassicCoreQueryOps; +import com.couchbase.client.core.error.InvalidArgumentException; import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.core.retry.RetryStrategy; @@ -44,11 +50,13 @@ import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.InsertOptions; -import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.MutateInOptions; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.kv.ScanOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -67,21 +75,21 @@ public class OptionsBuilder { static QueryOptions buildQueryOptions(Query query, QueryOptions options, QueryScanConsistency scanConsistency) { options = options != null ? options : QueryOptions.queryOptions(); if (query.getParameters() != null) { - if (query.getParameters() instanceof JsonArray) { + if (query.getParameters() instanceof JsonArray && !((JsonArray) query.getParameters()).isEmpty()) { options.parameters((JsonArray) query.getParameters()); - } else { + } else if( query.getParameters() instanceof JsonObject && !((JsonObject)query.getParameters()).isEmpty()){ options.parameters((JsonObject) query.getParameters()); } } Meta meta = query.getMeta() != null ? query.getMeta() : new Meta(); QueryOptions.Built optsBuilt = options.build(); - JsonObject optsJson = getQueryOpts(optsBuilt); + QueryScanConsistency metaQueryScanConsistency = meta.get(SCAN_CONSISTENCY) != null ? ((ScanConsistency) meta.get(SCAN_CONSISTENCY)).query() : null; QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, query.getScanConsistency(), - getScanConsistency(optsJson), scanConsistency, metaQueryScanConsistency); + scanConsistency(optsBuilt), scanConsistency, metaQueryScanConsistency); Duration timeout = fromFirst(Duration.ofSeconds(0), getTimeout(optsBuilt), meta.get(TIMEOUT)); RetryStrategy retryStrategy = fromFirst(null, getRetryStrategy(optsBuilt), meta.get(RETRY_STRATEGY)); @@ -100,6 +108,21 @@ static QueryOptions buildQueryOptions(Query query, QueryOptions options, QuerySc return options; } + private static QueryScanConsistency scanConsistency(QueryOptions.Built optsBuilt){ + CoreQueryScanConsistency scanConsistency = optsBuilt.scanConsistency(); + if (scanConsistency == null){ + return null; + } + switch (scanConsistency) { + case NOT_BOUNDED: + return QueryScanConsistency.NOT_BOUNDED; + case REQUEST_PLUS: + return QueryScanConsistency.REQUEST_PLUS; + default: + throw new InvalidArgumentException("Unknown scan consistency type " + scanConsistency, null, null); + } + } + public static TransactionQueryOptions buildTransactionQueryOptions(QueryOptions options) { QueryOptions.Built built = options.build(); TransactionQueryOptions txOptions = TransactionQueryOptions.queryOptions(); @@ -110,8 +133,21 @@ public static TransactionQueryOptions buildTransactionQueryOptions(QueryOptions throw new IllegalArgumentException("QueryOptions.flexIndex is not supported in a transaction"); } + Object value = optsJson.get("args"); + if(value instanceof JsonObject){ + txOptions.parameters((JsonObject)value); + }else if(value instanceof JsonArray) { + txOptions.parameters((JsonArray) value); + } else if(value != null) { + throw InvalidArgumentException.fromMessage( + "non-null args property was neither JsonObject(namedParameters) nor JsonArray(positionalParameters) " + + value); + } + for (Map.Entry entry : optsJson.toMap().entrySet()) { - txOptions.raw(entry.getKey(), entry.getValue()); + if(!entry.getKey().equals("args")) { + txOptions.raw(entry.getKey(), entry.getValue()); + } } if (LOG.isDebugEnabled()) { @@ -251,12 +287,14 @@ public static String getScopeFrom(Class domainType) { return null; } - public static DurabilityLevel getDurabilityLevel(Class domainType) { + public static DurabilityLevel getDurabilityLevel(Class domainType, CouchbaseConverter converter) { if (domainType == null) { return DurabilityLevel.NONE; } - Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class); - return document != null ? document.durabilityLevel() : DurabilityLevel.NONE; + final CouchbasePersistentEntity entity = converter.getMappingContext() + .getRequiredPersistentEntity(domainType); + + return entity.getDurabilityLevel(); } public static PersistTo getPersistTo(Class domainType) { @@ -370,10 +408,8 @@ static String toString(MutateInOptions o) { return s.toString(); } - private static JsonObject getQueryOpts(QueryOptions.Built optsBuilt) { - JsonObject jo = JsonObject.create(); - optsBuilt.injectParams(jo); - return jo; + public static JsonObject getQueryOpts(QueryOptions.Built optsBuilt) { + return JsonObject.fromJson(ClassicCoreQueryOps.convertOptions(optsBuilt).toString().getBytes()); } /** @@ -396,18 +432,6 @@ public static T fromFirst(T deflt, Object... choice) { return chosen; } - private static QueryScanConsistency getScanConsistency(JsonObject opts) { - String str = opts.getString("scan_consistency"); - if ("at_plus".equals(str)) { - return null; - } - return str == null ? null : QueryScanConsistency.valueOf(str.toUpperCase()); - } - - private static JsonObject getScanVectors(JsonObject opts) { - return opts.getObject("scan_vectors"); - } - private static Duration getTimeout(QueryOptions.Built optsBuilt) { Optional timeout = optsBuilt.timeout(); return timeout.isPresent() ? timeout.get() : null; @@ -522,4 +546,30 @@ public static String annotationString(Class annotation return annotationString(annotation, "value", defaultValue, elements); } + public static ScanOptions buildScanOptions(ScanOptions options, Object sort, Boolean idsOnly, + MutationState mutationState, Integer batchByteLimit, Integer batchItemLimit) { + options = options != null ? options : ScanOptions.scanOptions(); + if (sort != null) { + //options.sort(sort); + } + if (idsOnly != null) { + options.idsOnly(idsOnly); + } + if (mutationState != null) { + options.consistentWith(mutationState); + } + if (batchByteLimit != null) { + options.batchByteLimit(batchByteLimit); + } + if (batchItemLimit != null) { + options.batchItemLimit(batchItemLimit); + } + return options; + } + + public static CoreQueryContext queryContext(String scope, String collection, String bucketName) { + return (scope == null || CollectionIdentifier.DEFAULT_SCOPE.equals(scope)) + && (collection == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collection)) ? null + : CoreQueryContext.of(bucketName, scope); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index 48dd8fb81..592bd42d6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -34,7 +34,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.Alias; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -354,7 +353,9 @@ public String toN1qlSelectString(CouchbaseConverter converter, String bucketName domainClass, returnClass, isCount, distinctFields, fields); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.selectEntity); // select ... - appendWhereString(statement, n1ql.filter); // typeKey = typeValue + if (n1ql.filter != null) { + appendWhereString(statement, n1ql.filter); // typeKey = typeValue + } appendWhere(statement, new int[] { 0 }, converter); // criteria on this Query if (!isCount) { appendSort(statement); @@ -369,7 +370,9 @@ public String toN1qlRemoveString(CouchbaseConverter converter, String bucketName domainClass, null, false, null, null); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.delete); // delete ... - appendWhereString(statement, n1ql.filter); // typeKey = typeValue + if (n1ql.filter != null) { + appendWhereString(statement, n1ql.filter); // typeKey = typeValue + } appendWhere(statement, null, converter); // criteria on this Query appendString(statement, n1ql.returning); return statement.toString(); @@ -383,7 +386,7 @@ public static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(Couchb .getRequiredPersistentEntity(domainClass); MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); String typeValue = info.getJavaType().getName(); - TypeInformation typeInfo = ClassTypeInformation.from(info.getJavaType()); + TypeInformation typeInfo = TypeInformation.of(info.getJavaType()); Alias alias = converter.getTypeAlias(typeInfo); if (alias != null && alias.isPresent()) { typeValue = alias.toString(); @@ -418,6 +421,10 @@ public Meta getMeta() { return meta; } + public boolean isReadonly() { + return true; + } + public boolean equals(Object o) { if (!o.getClass().isAssignableFrom(getClass())) { return false; diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index 241d11e9d..abeff959d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -25,7 +25,7 @@ import java.util.Locale; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.CollectionUtils; import com.couchbase.client.core.error.CouchbaseException; @@ -38,6 +38,7 @@ * @author Michael Nitschinger * @author Michael Reiche * @author Mauro Monti + * @author Shubham Mishra */ public class QueryCriteria implements QueryCriteriaDefinition { @@ -148,13 +149,7 @@ public QueryCriteria and(N1QLExpression key) { } public QueryCriteria and(QueryCriteria criteria) { - if (this.criteriaChain != null && !this.criteriaChain.contains(this)) { - throw new RuntimeException("criteria chain does not include this"); - } - if (this.criteriaChain == null) { - this.criteriaChain = new LinkedList<>(); - this.criteriaChain.add(this); - } + checkAndAddToCriteriaChain(); QueryCriteria newThis = wrap(this); QueryCriteria qc = wrap(criteria); newThis.criteriaChain.add(qc); @@ -189,13 +184,7 @@ public QueryCriteria or(N1QLExpression key) { } public QueryCriteria or(QueryCriteria criteria) { - if (this.criteriaChain != null && !this.criteriaChain.contains(this)) { - throw new RuntimeException("criteria chain does not include this"); - } - if (this.criteriaChain == null) { - this.criteriaChain = new LinkedList<>(); - this.criteriaChain.add(this); - } + checkAndAddToCriteriaChain(); QueryCriteria newThis = wrap(this); QueryCriteria qc = wrap(criteria); qc.criteriaChain = newThis.criteriaChain; @@ -658,7 +647,7 @@ private static Object convert(CouchbaseConverter converter, Object value) { private void addAsCollection(JsonArray posValues, Collection collection, CouchbaseConverter converter) { JsonArray ja = JsonValue.ja(); for (Object e : collection) { - ja.add(String.valueOf(convert(converter, e))); + ja.add(convert(converter, e)); } posValues.add(ja); } @@ -719,4 +708,14 @@ public String toString() { sb.append("}"); return sb.toString(); } + + private void checkAndAddToCriteriaChain() { + if (this.criteriaChain != null && !this.criteriaChain.contains(this)) { + throw new RuntimeException("criteria chain does not include this"); + } + if (this.criteriaChain == null) { + this.criteriaChain = new LinkedList<>(); + this.criteriaChain.add(this); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java index 55e396866..5bc5a199a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 the original author or authors. + * Copyright 2010-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java index 7c294d092..43eb46920 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -25,10 +25,8 @@ import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.mapping.Alias; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; -import org.springframework.expression.spel.standard.SpelExpressionParser; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; @@ -36,33 +34,34 @@ /** * Query created from the string in @Query annotation in the repository interface. - * + * *
  * @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2")
  * List<User> getByFirstnameAndLastname(String firstname, String lastname);
  * 
- * + * * It must include the SELECT ... FROM ... preferably via the #n1ql expression, in addition to any predicates required, * including the n1ql.filter (for _class = className) - * + * * @author Michael Reiche */ public class StringQuery extends Query { private final CouchbaseQueryMethod queryMethod; private final String inlineN1qlQuery; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final ParameterAccessor parameterAccessor; - private final SpelExpressionParser spelExpressionParser; public StringQuery(CouchbaseQueryMethod queryMethod, String n1qlString, - QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider, ParameterAccessor parameterAccessor, - SpelExpressionParser spelExpressionParser) { + ValueExpressionDelegate valueExpressionDelegate, ParameterAccessor parameterAccessor) { this.queryMethod = queryMethod; this.inlineN1qlQuery = n1qlString; - this.evaluationContextProvider = queryMethodEvaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; this.parameterAccessor = parameterAccessor; - this.spelExpressionParser = spelExpressionParser; + } + + public StringQuery(String n1qlString) { + this(null,n1qlString, null, null); } @Override @@ -72,8 +71,7 @@ public String toN1qlSelectString(CouchbaseConverter converter, String bucketName StringBasedN1qlQueryParser parser = getStringN1qlQueryParser(converter, bucketName, scope, collection, domainClass, distinctFields, fields); - N1QLExpression parsedExpression = parser.getExpression(inlineN1qlQuery, queryMethod, parameterAccessor, - spelExpressionParser, evaluationContextProvider); + N1QLExpression parsedExpression = parser.getExpression(inlineN1qlQuery, queryMethod, parameterAccessor, valueExpressionDelegate); String queryString = parsedExpression.toString(); @@ -118,22 +116,29 @@ private StringBasedN1qlQueryParser getStringN1qlQueryParser(CouchbaseConverter c .getRequiredPersistentEntity(domainClass); MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); String typeValue = info.getJavaType().getName(); - TypeInformation typeInfo = ClassTypeInformation.from(info.getJavaType()); + TypeInformation typeInfo = TypeInformation.of(info.getJavaType()); Alias alias = converter.getTypeAlias(typeInfo); if (alias != null && alias.isPresent()) { typeValue = alias.toString(); } // there are no options for distinct and fields for @Query StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(inlineN1qlQuery, queryMethod, bucketName, - scopeName, collectionName, converter, typeKey, typeValue, parameterAccessor, new SpelExpressionParser(), - evaluationContextProvider); + scopeName, collectionName, converter, typeKey, typeValue, parameterAccessor, valueExpressionDelegate); return sbnqp; } + @Override + public boolean isReadonly() { + if (this.queryMethod.hasN1qlAnnotation()) { + return this.queryMethod.getN1qlAnnotation().readonly(); + } + return false; + } + /** * toN1qlRemoveString - use toN1qlSelectString - * + * * @param converter * @param bucketName * @param scopeName diff --git a/src/main/java/org/springframework/data/couchbase/core/query/View.java b/src/main/java/org/springframework/data/couchbase/core/query/View.java index 492f4bc4f..eb0cff9fc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/View.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/View.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java index 47adf93fe..d454ed6e2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java b/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java index e3af4dad8..6f35f1001 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java b/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java index 451e2abc8..bdb1d2b7a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java index a0bdd0220..28a8374ed 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/ConsistentWith.java b/src/main/java/org/springframework/data/couchbase/core/support/ConsistentWith.java new file mode 100644 index 000000000..43fd47da7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/ConsistentWith.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020-2025 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.couchbase.core.support; + +import com.couchbase.client.java.kv.MutationState; + +/** + * A common interface for those that support withOptions() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface ConsistentWith { + Object consistentWith(MutationState mutationState); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/InCollection.java b/src/main/java/org/springframework/data/couchbase/core/support/InCollection.java index f0fde1213..2faf860ad 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/InCollection.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/InCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/InScope.java b/src/main/java/org/springframework/data/couchbase/core/support/InScope.java index 4c0439a52..10b3b573b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/InScope.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/InScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java index 60872c4bf..3ce652e75 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java index efc139880..89b0dfac0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java index 1b013528a..c0c614a20 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java index 711acc170..9a8cd5053 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java index ce8aa4d3f..00bf17d06 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java index 73c994838..f45304fdb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java index c0abee865..dad971732 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java index eacff0f28..cb21779e7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index 64ab1a0eb..2e3db313f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java b/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java index 67185bf52..a157dc5fc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java index 97b09b640..1384d0c79 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsOptions.java index dac23522d..60a3c0905 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java index 95da11ad6..786458c2e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithBatchByteLimit.java b/src/main/java/org/springframework/data/couchbase/core/support/WithBatchByteLimit.java new file mode 100644 index 000000000..9f8b855c9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithBatchByteLimit.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2025 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.couchbase.core.support; + +/** + * A common interface for those that support withBatchByteLimit() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithBatchByteLimit { + Object withBatchByteLimit(Integer batchByteLimit); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithBatchItemLimit.java b/src/main/java/org/springframework/data/couchbase/core/support/WithBatchItemLimit.java new file mode 100644 index 000000000..cd844a87e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithBatchItemLimit.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2025 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.couchbase.core.support; + +/** + * A common interface for those that support withBatchItemLimit() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithBatchItemLimit { + Object withBatchItemLimit(Integer batchItemLimit); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java b/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java index cfa84a4e3..98739a813 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java b/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java index 5d161a5fd..8b85ce527 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithDurability.java b/src/main/java/org/springframework/data/couchbase/core/support/WithDurability.java index 2b0d36605..52e5201e4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithDurability.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithDurability.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithExistsOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithExistsOptions.java index b6997bcc4..651e4e175 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithExistsOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithExistsOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithExpiry.java b/src/main/java/org/springframework/data/couchbase/core/support/WithExpiry.java index 98c51e10d..dd8aed9f7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithExpiry.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithExpiry.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithGetAnyReplicaOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithGetAnyReplicaOptions.java index 9079d1564..34605040b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithGetAnyReplicaOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithGetAnyReplicaOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithGetOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithGetOptions.java index d3af86db8..2aca9a984 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithGetOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithGetOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithInsertOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithInsertOptions.java index c5d4beee5..af39354f4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithInsertOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithInsertOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithLock.java b/src/main/java/org/springframework/data/couchbase/core/support/WithLock.java index e23bb6b97..db37fd548 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithLock.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithLock.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java index ffd39229f..601930300 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java index 90c53b6d5..063c94a3d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java index 6cd0c9b48..283da2b93 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjecting.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java index 8a8e139ca..cca67be04 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java index ec7468fd5..50cc22531 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java b/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java index 3e9b61b51..02e1f14d8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithQueryOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithQueryOptions.java index 24af8eeb7..2547e023c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithQueryOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithQueryOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithRemoveOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithRemoveOptions.java index d87ee7fa2..c1140a8d6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithRemoveOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithRemoveOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithReplaceOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithReplaceOptions.java index 9abf18072..3982bec8a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithReplaceOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithReplaceOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithScanOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithScanOptions.java new file mode 100644 index 000000000..9448dd8d8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithScanOptions.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2025 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.couchbase.core.support; + +import com.couchbase.client.java.kv.ScanOptions; + +/** + * A common interface for those that support withOptions() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithScanOptions { + Object withOptions(ScanOptions expiry); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithScanSort.java b/src/main/java/org/springframework/data/couchbase/core/support/WithScanSort.java new file mode 100644 index 000000000..183914fcf --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithScanSort.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2025 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.couchbase.core.support; + +/** + * A common interface for those that support withOptions() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithScanSort { + Object withSort(Object expiry); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithUpsertOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithUpsertOptions.java index df465f6f2..629767711 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithUpsertOptions.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithUpsertOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. diff --git a/src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java b/src/main/java/org/springframework/data/couchbase/querydsl/document/AbstractCouchbaseQueryDSL.java similarity index 98% rename from src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java rename to src/main/java/org/springframework/data/couchbase/querydsl/document/AbstractCouchbaseQueryDSL.java index aee2c184e..62a1c6b98 100644 --- a/src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java +++ b/src/main/java/org/springframework/data/couchbase/querydsl/document/AbstractCouchbaseQueryDSL.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.querydsl.couchbase.document; +package org.springframework.data.couchbase.querydsl.document; import java.util.Collection; import java.util.HashMap; diff --git a/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java b/src/main/java/org/springframework/data/couchbase/querydsl/document/CouchbaseDocumentSerializer.java similarity index 99% rename from src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java rename to src/main/java/org/springframework/data/couchbase/querydsl/document/CouchbaseDocumentSerializer.java index 310c67529..866f951bf 100644 --- a/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java +++ b/src/main/java/org/springframework/data/couchbase/querydsl/document/CouchbaseDocumentSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.querydsl.couchbase.document; +package org.springframework.data.couchbase.querydsl.document; import java.util.Collection; import java.util.List; diff --git a/src/main/java/org/springframework/data/couchbase/repository/Collection.java b/src/main/java/org/springframework/data/couchbase/repository/Collection.java index 7b9424588..3f7565124 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/Collection.java +++ b/src/main/java/org/springframework/data/couchbase/repository/Collection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java index d702acb1a..170a5051d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index 9b0674cb9..a3c40e4a9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/Options.java b/src/main/java/org/springframework/data/couchbase/repository/Options.java index 5ae84655e..a01609e08 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/Options.java +++ b/src/main/java/org/springframework/data/couchbase/repository/Options.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/Query.java b/src/main/java/org/springframework/data/couchbase/repository/Query.java index 46d417dec..35440eb75 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/Query.java +++ b/src/main/java/org/springframework/data/couchbase/repository/Query.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -58,4 +58,11 @@ */ String value() default ""; + /** + * Mark query as readonly + * + * @see com.couchbase.client.java.query.QueryOptions#readonly(boolean) + */ + boolean readonly() default false; + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index a9108b38a..8a337e65b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java b/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java index 714e47567..96f4a9db4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/Scope.java b/src/main/java/org/springframework/data/couchbase/repository/Scope.java index 175462fe4..bcd9e7e08 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/Scope.java +++ b/src/main/java/org/springframework/data/couchbase/repository/Scope.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java index f025fc3ef..7513514be 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java index b322dd785..bc16d5e67 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableReactiveCouchbaseAuditing.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableReactiveCouchbaseAuditing.java index 6b403b8b9..eead779ac 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableReactiveCouchbaseAuditing.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableReactiveCouchbaseAuditing.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/PersistentEntitiesFactoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/PersistentEntitiesFactoryBean.java index cca15a0cc..c4533e4d4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/PersistentEntitiesFactoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/PersistentEntitiesFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java index 47ce95322..ee4db995f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java index 5b6f7a090..a1dd976c8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryExtension.java b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryExtension.java index de09e7759..bac5e45ef 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryExtension.java +++ b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoriesRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoriesRegistrar.java index 2a488e721..716237390 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java index 0ac2f1d28..2f7888dae 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/EnableCouchbaseRepositories.java b/src/main/java/org/springframework/data/couchbase/repository/config/EnableCouchbaseRepositories.java index 6a48dedbe..d0e12943e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/EnableCouchbaseRepositories.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/EnableCouchbaseRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/EnableReactiveCouchbaseRepositories.java b/src/main/java/org/springframework/data/couchbase/repository/config/EnableReactiveCouchbaseRepositories.java index d47b45d3b..0bf52ed11 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/EnableReactiveCouchbaseRepositories.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/EnableReactiveCouchbaseRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoriesRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoriesRegistrar.java index 20a869ab4..8359781b9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoryConfigurationExtension.java index a31b64376..9b6f9815d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveCouchbaseRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveRepositoryOperationsMapping.java b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveRepositoryOperationsMapping.java index 96280df80..b9a983a1c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveRepositoryOperationsMapping.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/ReactiveRepositoryOperationsMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/RepositoryOperationsMapping.java b/src/main/java/org/springframework/data/couchbase/repository/config/RepositoryOperationsMapping.java index 4e228efb6..44c121642 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/RepositoryOperationsMapping.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/RepositoryOperationsMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/package-info.java b/src/main/java/org/springframework/data/couchbase/repository/package-info.java index 05057d1b5..5a26eed86 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/package-info.java +++ b/src/main/java/org/springframework/data/couchbase/repository/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java index 8aa357623..e2dade1bb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -27,11 +27,10 @@ import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -52,16 +51,14 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. */ public AbstractCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, operations, expressionParser, evaluationContextProvider); + ValueExpressionDelegate valueExpressionDelegate) { + super(method, operations, valueExpressionDelegate); Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + Assert.notNull(valueExpressionDelegate, "QueryMethodEvaluationContextProvider must not be null!"); EntityMetadata metadata = method.getEntityInformation(); Class type = metadata.getJavaType(); this.findOp = (ExecutableFindByQuery) (operations.findByQuery(type).inScope(method.getScope()) diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java index f8cfd670b..78452f5b5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -26,13 +26,11 @@ import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -48,8 +46,7 @@ public abstract class AbstractCouchbaseQueryBase implem private final CouchbaseOperationsType operations; private final EntityInstantiators instantiators; private final ExecutableFindByQuery findOperationWithProjection; - private final SpelExpressionParser expressionParser; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; /** * Creates a new {@link AbstractCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and @@ -57,22 +54,19 @@ public abstract class AbstractCouchbaseQueryBase implem * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. */ public AbstractCouchbaseQueryBase(CouchbaseQueryMethod method, CouchbaseOperationsType operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + ValueExpressionDelegate valueExpressionDelegate) { Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null!"); this.method = method; this.operations = operations; this.instantiators = new EntityInstantiators(); - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; EntityMetadata metadata = method.getEntityInformation(); Class type = metadata.getJavaType(); @@ -122,8 +116,8 @@ private Publisher executeDeferred(ReactiveCouchbaseParameterAccessor par } private Object execute(ParametersParameterAccessor parameterAccessor) { - TypeInformation returnType = ClassTypeInformation - .from(method.getResultProcessor().getReturnedType().getReturnedType()); + TypeInformation returnType = TypeInformation + .of(method.getResultProcessor().getReturnedType().getReturnedType()); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); Class typeToRead = processor.getReturnedType().getTypeToRead(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index 343900f77..30fcb4c53 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java index 874a3adfa..0aa8fca16 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -26,11 +26,10 @@ import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -51,16 +50,13 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. */ public AbstractReactiveCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, operations, expressionParser, evaluationContextProvider); + ValueExpressionDelegate valueExpressionDelegate) { + super(method, operations, valueExpressionDelegate); Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); EntityMetadata metadata = method.getEntityInformation(); Class type = metadata.getJavaType(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java b/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java index db01c1f27..0bfb7ceeb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseEntityInformation.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseEntityInformation.java index bf573ada1..a6e8491df 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseEntityInformation.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbasePartTree.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbasePartTree.java index fc7912f42..d4344a1cb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbasePartTree.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbasePartTree.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java index 16c8eb335..e9478be35 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index 01156ed1a..480faa141 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java index 6326e39d4..ae6a6e0d6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -18,8 +18,8 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Michael Nitschinger @@ -32,19 +32,19 @@ public class CouchbaseRepositoryQuery implements RepositoryQuery { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; - this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; + this.valueExpressionDelegate = ValueExpressionDelegate.create(); } @Override public Object execute(final Object[] parameters) { - return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, valueExpressionDelegate) .execute(parameters); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java b/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java index e83e15dda..32f7f9c6b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java index d0995df37..0456e9519 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -21,6 +21,7 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; @@ -44,12 +45,17 @@ protected N1QLExpression complete(N1QLExpression criteria, Sort sort) { private static class CountParameterAccessor implements ParameterAccessor { - private ParameterAccessor delegate; + private final ParameterAccessor delegate; public CountParameterAccessor(ParameterAccessor delegate) { this.delegate = delegate; } + @Override + public ScrollPosition getScrollPosition() { + return delegate.getScrollPosition(); + } + public Pageable getPageable() { return delegate.getPageable().isPaged() ? new CountPageable(delegate.getPageable()) : Pageable.unpaged(); } @@ -79,7 +85,7 @@ public Iterator iterator() { private static class CountPageable implements Pageable { - private Pageable delegate; + private final Pageable delegate; public CountPageable(Pageable delegate) { this.delegate = delegate; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java index 28ad39d11..c56feba96 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. @@ -37,6 +37,7 @@ * supported * * @author Subhashni Balakrishnan + * @author Michael Reiche */ public class N1qlMutateQueryCreator extends AbstractQueryCreator implements PartTreeN1qlQueryCreator { @@ -82,6 +83,9 @@ protected N1QLExpression or(N1QLExpression base, N1QLExpression criteria) { protected N1QLExpression complete(N1QLExpression criteria, Sort sort) { N1QLExpression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter, this.queryMethod.getEntityInformation()); + if (whereCriteria == null) { + return mutateFrom; + } return mutateFrom.where(whereCriteria); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index 98674db7f..180fe56ca 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java index d52e51a5b..50f24c33f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -22,7 +22,7 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -39,14 +39,14 @@ public class N1qlRepositoryQueryExecutor { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, - final NamedQueries namedQueries, final QueryMethodEvaluationContextProvider evaluationContextProvider) { + final NamedQueries namedQueries, final ValueExpressionDelegate valueExpressionDelegate) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; } private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); @@ -69,8 +69,8 @@ public Object execute(final Object[] parameters) { Query query; ExecutableFindByQuery q; if (queryMethod.hasN1qlAnnotation()) { - query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), SPEL_PARSER, - evaluationContextProvider, namedQueries).createQuery(); + query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), + valueExpressionDelegate, namedQueries).createQuery(); } else { final PartTree tree = new PartTree(queryMethod.getName(), domainClass); query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter(), operations.getBucketName()) diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/OldN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/OldN1qlQueryCreator.java index 9a89a4181..60d962bdd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/OldN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/OldN1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -124,7 +124,7 @@ protected N1QLExpression complete(N1QLExpression criteria, Sort sort) { N1QLExpression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter, this.queryMethod.getEntityInformation()); - N1QLExpression selectFromWhere = selectFrom.where(whereCriteria); + N1QLExpression selectFromWhere = whereCriteria != null ? selectFrom.where(whereCriteria) : selectFrom; // sort of the Pageable takes precedence over the sort in the query name if ((queryMethod.isPageQuery() || queryMethod.isSliceQuery()) && accessor.getPageable().isPaged()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java index 891d19787..ebe972cc7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -21,11 +21,10 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; 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.PartTree; -import org.springframework.expression.spel.standard.SpelExpressionParser; /** * {@link RepositoryQuery} implementation for Couchbase. Replaces PartTreeN1qlBasedQuery @@ -43,13 +42,11 @@ public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery { * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. */ - public PartTreeCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + public PartTreeCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, ValueExpressionDelegate valueExpressionDelegate) { - super(method, operations, expressionParser, evaluationContextProvider); + super(method, operations, valueExpressionDelegate); ResultProcessor processor = method.getResultProcessor(); this.tree = new CouchbasePartTree(method.getName(), processor.getReturnedType().getDomainType()); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index 24acc86ac..92aa03865 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlQueryCreator.java index e53ffbb9c..681096ba5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors + * Copyright 2018-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java index 2a5d0625d..e8084cebe 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseParameterAccessor.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseParameterAccessor.java index 0495ff272..e2b6126be 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseParameterAccessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -28,7 +28,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; /** * Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java index 422d9eaff..11813f840 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java index add637305..846e0b4f0 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -15,8 +15,6 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.repository.util.ClassUtils.hasParameterOfType; - import java.lang.reflect.Method; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -29,9 +27,9 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.util.ReactiveWrappers; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Lazy; +import org.springframework.data.util.ReactiveWrappers; +import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; @@ -43,10 +41,9 @@ */ public class ReactiveCouchbaseQueryMethod extends CouchbaseQueryMethod { - private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class); - private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); + private static final TypeInformation PAGE_TYPE = TypeInformation.of(Page.class); + private static final TypeInformation SLICE_TYPE = TypeInformation.of(Slice.class); - private final Method method; private final Lazy isCollectionQueryCouchbase; // not to be confused with QueryMethod.isCollectionQuery /** @@ -62,9 +59,9 @@ public ReactiveCouchbaseQueryMethod(Method method, RepositoryMetadata metadata, super(method, metadata, projectionFactory, mappingContext); - if (hasParameterOfType(method, Pageable.class)) { + if (ReflectionUtils.hasParameterOfType(method, Pageable.class)) { - TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + TypeInformation returnType = TypeInformation.fromReturnTypeOf(method); boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) @@ -83,13 +80,12 @@ public ReactiveCouchbaseQueryMethod(Method method, RepositoryMetadata metadata, method.toString())); } - if (hasParameterOfType(method, Sort.class)) { + if (ReflectionUtils.hasParameterOfType(method, Sort.class)) { throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString())); } } - this.method = method; this.isCollectionQueryCouchbase = Lazy.of(() -> { boolean result = !(isPageQuery() || isSliceQuery()) && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType()); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java index c418901c5..3a4987a64 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -19,8 +19,7 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Michael Nitschinger @@ -33,20 +32,20 @@ public class ReactiveCouchbaseRepositoryQuery extends AbstractReactiveCouchbaseQ private final ReactiveCouchbaseOperations operations; private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { - super(queryMethod, operations, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT); + super(queryMethod, operations, ValueExpressionDelegate.create()); this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; - this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; + this.valueExpressionDelegate = ValueExpressionDelegate.create(); } @Override public Object execute(final Object[] parameters) { - return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, valueExpressionDelegate) .execute(parameters); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java index ca62400cb..3a70ea209 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -17,8 +17,7 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Michael Nitschinger @@ -31,15 +30,15 @@ public class ReactiveN1qlRepositoryQueryExecutor { private final ReactiveCouchbaseOperations operations; private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations operations, final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + ValueExpressionDelegate valueExpressionDelegate) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; } /** @@ -52,11 +51,9 @@ public Object execute(final Object[] parameters) { // counterpart to N1qlRespositoryQueryExecutor, if (queryMethod.hasN1qlAnnotation()) { - return new ReactiveStringBasedCouchbaseQuery(queryMethod, operations, new SpelExpressionParser(), - evaluationContextProvider, namedQueries).execute(parameters); + return new ReactiveStringBasedCouchbaseQuery(queryMethod, operations, valueExpressionDelegate, namedQueries).execute(parameters); } else { - return new ReactivePartTreeCouchbaseQuery(queryMethod, operations, new SpelExpressionParser(), - evaluationContextProvider).execute(parameters); + return new ReactivePartTreeCouchbaseQuery(queryMethod, operations, valueExpressionDelegate).execute(parameters); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java index d570d5b62..4e5457197 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -23,10 +23,9 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Reactive PartTree {@link RepositoryQuery} implementation for Couchbase. Replaces ReactivePartN1qlBasedQuery @@ -46,13 +45,12 @@ public class ReactivePartTreeCouchbaseQuery extends AbstractReactiveCouchbaseQue * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. */ public ReactivePartTreeCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + ValueExpressionDelegate valueExpressionDelegate) { - super(method, operations, expressionParser, evaluationContextProvider); + super(method, operations, valueExpressionDelegate); this.tree = new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()); this.converter = operations.getConverter(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java index d9e7b5fae..1246d4f63 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java index 5fd0761ec..5fd56696e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -21,9 +21,7 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.util.Assert; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Query to use a plain JSON String to create the {@link Query} to actually execute. @@ -36,30 +34,24 @@ public class ReactiveStringBasedCouchbaseQuery extends AbstractReactiveCouchbase private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedCouchbaseQuery.class); - private final SpelExpressionParser expressionParser; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final NamedQueries namedQueries; /** * Creates a new {@link ReactiveStringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, - * {@link ReactiveCouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * {@link ReactiveCouchbaseOperations}, and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param couchbaseOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. * @param namedQueries must not be {@literal null}. */ public ReactiveStringBasedCouchbaseQuery(ReactiveCouchbaseQueryMethod method, - ReactiveCouchbaseOperations couchbaseOperations, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) { + ReactiveCouchbaseOperations couchbaseOperations, ValueExpressionDelegate valueExpressionDelegate, NamedQueries namedQueries) { - super(method, couchbaseOperations, expressionParser, evaluationContextProvider); + super(method, couchbaseOperations, valueExpressionDelegate); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; if (hasAmbiguousProjectionFlags(isCountQuery(), isExistsQuery(), isDeleteQuery())) { throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); @@ -77,7 +69,7 @@ public ReactiveStringBasedCouchbaseQuery(ReactiveCouchbaseQueryMethod method, protected Query createQuery(ParametersParameterAccessor accessor) { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), - getOperations().getConverter(), expressionParser, evaluationContextProvider, namedQueries); + getOperations().getConverter(), valueExpressionDelegate, namedQueries); Query query = creator.createQuery(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java index 2ecc205b1..37f152116 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java index 68337933b..82fdd86f5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -21,9 +21,8 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.util.Assert; /** * Query to use a plain JSON String to create the {@link Query} to actually execute. @@ -36,30 +35,25 @@ public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery { private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; private static final Logger LOG = LoggerFactory.getLogger(StringBasedCouchbaseQuery.class); - private final SpelExpressionParser expressionParser; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final NamedQueries namedQueries; /** * Creates a new {@link StringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, - * {@link CouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * {@link CouchbaseOperations}, {@link SpelExpressionParser} and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param couchbaseOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null}. * @param namedQueries must not be {@literal null}. */ public StringBasedCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations couchbaseOperations, - SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider, + ValueExpressionDelegate valueExpressionDelegate, NamedQueries namedQueries) { - super(method, couchbaseOperations, expressionParser, evaluationContextProvider); + super(method, couchbaseOperations, valueExpressionDelegate); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - - this.expressionParser = expressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; if (hasAmbiguousProjectionFlags(isCountQuery(), isExistsQuery(), isDeleteQuery())) { throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); @@ -75,7 +69,7 @@ public StringBasedCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperation protected Query createQuery(ParametersParameterAccessor accessor) { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), - getOperations().getConverter(), expressionParser, evaluationContextProvider, namedQueries); + getOperations().getConverter(), valueExpressionDelegate, namedQueries); Query query = creator.createQuery(); if (LOG.isTraceEnabled()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index 082e12766..0c298a6f1 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -15,10 +15,8 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.i; -import static org.springframework.data.couchbase.core.query.N1QLExpression.x; -import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS; -import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID; +import static org.springframework.data.couchbase.core.query.N1QLExpression.*; +import static org.springframework.data.couchbase.core.support.TemplateUtils.*; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -32,23 +30,28 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseList; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.Expiration; import org.springframework.data.couchbase.core.query.N1QLExpression; +import org.springframework.data.couchbase.core.query.StringQuery; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.common.TemplateParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; @@ -60,6 +63,7 @@ /** * @author Subhashni Balakrishnan * @author Michael Reiche + * @author Tigran Babloyan */ public class StringBasedN1qlQueryParser { public static final String SPEL_PREFIX = "n1ql"; @@ -122,6 +126,12 @@ public class StringBasedN1qlQueryParser { * regexp that detect positional placeholder ($ followed by digits only) */ public static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b"); + + /** + * regexp that detect SPEL Expression (#{..}) + */ + public static final Pattern SPEL_EXPRESSION_PATTERN = Pattern.compile("(#\\{[^\\}]*\\})"); + /** * regexp that detects " and ' quote boundaries, ignoring escaped quotes */ @@ -147,20 +157,18 @@ public class StringBasedN1qlQueryParser { * @param typeField * @param typeValue * @param accessor - * @param spelExpressionParser - * @param evaluationContextProvider + * @param valueExpressionDelegate */ public StringBasedN1qlQueryParser(String statement, CouchbaseQueryMethod queryMethod, String bucketName, String scope, String collection, CouchbaseConverter couchbaseConverter, String typeField, String typeValue, - ParameterAccessor accessor, SpelExpressionParser spelExpressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + ParameterAccessor accessor, ValueExpressionDelegate valueExpressionDelegate) { this.statement = statement; this.queryMethod = queryMethod; this.couchbaseConverter = couchbaseConverter; - this.statementContext = createN1qlSpelValues(collection != null ? collection : bucketName, scope, collection, - queryMethod.getEntityInformation().getJavaType(), typeField, typeValue, queryMethod.isCountQuery(), null, null); - this.parsedExpression = getExpression(statement, queryMethod, accessor, spelExpressionParser, - evaluationContextProvider); + this.statementContext = queryMethod == null ? null + : createN1qlSpelValues(bucketName, scope, collection, queryMethod.getEntityInformation().getJavaType(), + typeField, typeValue, queryMethod.isCountQuery(), null, null); + this.parsedExpression = getExpression(statement, queryMethod, accessor, valueExpressionDelegate); } /** @@ -198,7 +206,7 @@ public StringBasedN1qlQueryParser(String bucketName, String scope, String collec * @param scope * @param collection * @param domainClass - * @param typeField + * @param typeKey * @param typeValue * @param isCount * @param distinctFields @@ -206,7 +214,7 @@ public StringBasedN1qlQueryParser(String bucketName, String scope, String collec * @return */ public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, String collection, Class domainClass, - String typeField, String typeValue, boolean isCount, String[] distinctFields, String[] fields) { + String typeKey, String typeValue, boolean isCount, String[] distinctFields, String[] fields) { String b = bucketName; String keyspace = collection != null ? collection : bucketName; Assert.isTrue(!(distinctFields != null && fields != null), @@ -214,7 +222,7 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, Stri String entityFields = ""; String selectEntity; if (distinctFields != null) { - String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields); + String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeKey, fields, distinctFields); if (isCount) { selectEntity = N1QLExpression.select(N1QLExpression.count(N1QLExpression.distinct(x(distinctFieldsStr))) .as(i(CountFragment.COUNT_ALIAS)).from(keyspace)).toString(); @@ -225,11 +233,11 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, Stri selectEntity = N1QLExpression.select(N1QLExpression.count(x("\"*\"")).as(i(CountFragment.COUNT_ALIAS))) .from(keyspace).toString(); } else { - String projectedFields = getProjectedOrDistinctFields(keyspace, domainClass, typeField, fields, distinctFields); + String projectedFields = getProjectedOrDistinctFields(keyspace, domainClass, typeKey, fields, distinctFields); entityFields = projectedFields; selectEntity = N1QLExpression.select(x(projectedFields)).from(keyspace).toString(); } - String typeSelection = "`" + typeField + "` = \"" + typeValue + "\""; + String typeSelection = !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)).toString() : null; String delete = N1QLExpression.delete().from(keyspace).toString(); String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(keyspace); @@ -238,6 +246,10 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, Stri i(collection).toString(), typeSelection, delete, returning); } + private static boolean empty(String s) { + return s == null || s.length() == 0; + } + private String getProjectedOrDistinctFields(String b, Class resultClass, String typeField, String[] fields, String[] distinctFields) { if (distinctFields != null && distinctFields.length != 0) { @@ -317,7 +329,13 @@ private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentPr if (fieldList == null || fieldList.contains(prop.getFieldName())) { PersistentPropertyPath path = couchbaseConverter.getMappingContext() .getPersistentPropertyPath(prop.getName(), persistentEntity.getTypeInformation().getType()); - projectField = N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop, persistentEntity).toString(); + String unmangled = prop.getFieldName(); + String maybeMangled =((MappingCouchbaseConverter)couchbaseConverter).maybeMangle(prop); + if(maybeMangled.equals(unmangled)) { + projectField = N1qlQueryCreator.addMetaIfRequired(bucketName, path, prop, persistentEntity).toString(); + } else { + projectField = i(maybeMangled).toString(); + } if (sb.length() > 0) { sb.append(", "); } @@ -358,12 +376,16 @@ private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentPr // this static method can be used to test the parsing behavior for Couchbase specific spel variables // in isolation from the rest of the spel parser initialization chain. - public static String doParse(String statement, SpelExpressionParser parser, EvaluationContext evaluationContext, - N1qlSpelValues n1qlSpelValues) { - org.springframework.expression.Expression parsedExpression = parser.parseExpression(statement, - new TemplateParserContext()); - evaluationContext.setVariable(SPEL_PREFIX, n1qlSpelValues); - return parsedExpression.getValue(evaluationContext, String.class); + public static String doParse(String statement, ValueExpressionParser parser, + ValueEvaluationContext valueEvaluationContext, N1qlSpelValues n1qlSpelValues) { + ValueExpression parsedExpression = parser.parse(statement); + + EvaluationContext evaluationContext = valueEvaluationContext.getRequiredEvaluationContext(); + if (evaluationContext instanceof StandardEvaluationContext ctx) { + ctx.setVariable(SPEL_PREFIX, n1qlSpelValues); + } + Object result = parsedExpression.evaluate(valueEvaluationContext); + return result != null ? result.toString() : null; } private void checkPlaceholders(String statement) { @@ -371,6 +393,8 @@ private void checkPlaceholders(String statement) { Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement); Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement); Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement); + String queryIdentifier = (this.queryMethod != null ? queryMethod.getClass().getName() : StringQuery.class.getName()) + + "." + (this.queryMethod != null ? queryMethod.getName() : this.statement); List quotes = new ArrayList(); while (quoteMatcher.find()) { @@ -383,8 +407,13 @@ private void checkPlaceholders(String statement) { while (positionMatcher.find()) { String placeholder = positionMatcher.group(1); // check not in quoted - if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) { - LOGGER.trace("{}: Found positional placeholder {}", this.queryMethod.getName(), placeholder); + if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes, queryIdentifier)) { + if (this.queryMethod == null) { + throw new IllegalArgumentException( + "StringQuery created from StringQuery(String) cannot have parameters. " + "They cannot be processed. " + + "Use an @Query annotated method and the SPEL Expression #{[]} : " + statement); + } + LOGGER.trace("{}: Found positional placeholder {}", queryIdentifier, placeholder); posCount++; parameterNames.add(placeholder.substring(1)); // save without the leading $ } @@ -393,8 +422,12 @@ private void checkPlaceholders(String statement) { while (namedMatcher.find()) { String placeholder = namedMatcher.group(1); // check not in quoted - if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) { - LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder); + if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes, queryIdentifier)) { + if (this.queryMethod == null) { + throw new IllegalArgumentException("StringQuery created from StringQuery(String) cannot have parameters. " + + "Use an @Query annotated method and the SPEL Expression #{[]} : " + statement); + } + LOGGER.trace("{}: Found named placeholder {}", queryIdentifier, placeholder); namedCount++; parameterNames.add(placeholder.substring(1));// save without the leading $ } @@ -402,8 +435,7 @@ private void checkPlaceholders(String statement) { if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount - + ") placeholders is not supported, please choose one over the other in " + queryMethod.getClass().getName() - + "." + this.queryMethod.getName() + "()"); + + ") placeholders is not supported, please choose one over the other in " + queryIdentifier + "()"); } if (posCount > 0) { @@ -413,12 +445,28 @@ private void checkPlaceholders(String statement) { } else { placeHolderType = PlaceholderType.NONE; } + + if (this.queryMethod == null) { + Matcher spelMatcher = SPEL_EXPRESSION_PATTERN.matcher(statement); + while (spelMatcher.find()) { + String placeholder = spelMatcher.group(1); + // check not in quoted + if (checkNotQuoted(placeholder, spelMatcher.start(), spelMatcher.end(), quotes, queryIdentifier)) { + if (this.queryMethod == null) { + throw new IllegalArgumentException("StringQuery created from StringQuery(String) cannot SPEL expressions. " + + "Use an @Query annotated method and the SPEL Expression #{[]} : " + statement); + } + LOGGER.trace("{}: Found SPEL Experssion {}", queryIdentifier, placeholder); + } + } + } + } - private boolean checkNotQuoted(String item, int start, int end, List quotes) { + private boolean checkNotQuoted(String item, int start, int end, List quotes, String queryIdentifier) { for (int[] quote : quotes) { if (quote[0] <= start && quote[1] >= end) { - LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item); + LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", queryIdentifier, item); return false; } } @@ -540,7 +588,7 @@ private void addAsArray(JsonObject namedValues, String placeholder, Object o) { Object[] array = (Object[]) o; JsonArray ja = JsonValue.ja(); for (Object e : array) { - ja.add(String.valueOf(couchbaseConverter.convertForWriteIfNeeded(e))); + ja.add(couchbaseConverter.convertForWriteIfNeeded(e)); } namedValues.put(placeholder, ja); } @@ -640,17 +688,21 @@ public N1qlSpelValues(String selectClause, String entityFields, String bucket, S * @param statement * @param queryMethod * @param accessor - * @param parser - * @param evaluationContextProvider + * @param valueExpressionDelegate * @return */ public N1QLExpression getExpression(String statement, CouchbaseQueryMethod queryMethod, ParameterAccessor accessor, - SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - Object[] runtimeParameters = getParameters(accessor); - EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), - runtimeParameters); - N1QLExpression parsedStatement = x(doParse(statement, parser, evaluationContext, this.getStatementContext())); + ValueExpressionDelegate valueExpressionDelegate) { + N1QLExpression parsedStatement; + if (accessor != null && queryMethod != null) { + Object[] runtimeParameters = getParameters(accessor); + ValueEvaluationContext evaluationContext = valueExpressionDelegate + .createValueContextProvider(queryMethod.getParameters()).getEvaluationContext(runtimeParameters); + parsedStatement = x(doParse(statement, valueExpressionDelegate, evaluationContext, this.getStatementContext())); + } else { + parsedStatement = x(statement); + } checkPlaceholders(parsedStatement.toString()); return parsedStatement; } @@ -660,8 +712,11 @@ private static Object[] getParameters(ParameterAccessor accessor) { for (Object o : accessor) { params.add(o); } - params.add(accessor.getPageable()); - params.add(accessor.getSort()); + if (accessor.getPageable().isPaged()) { + params.add(accessor.getPageable()); + } else if (accessor.getSort().isSorted()) { + params.add(accessor.getSort()); + } return params.toArray(); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java index a4d480154..6838ea13f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -15,8 +15,8 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.x; -import static org.springframework.data.couchbase.core.query.QueryCriteria.where; +import static org.springframework.data.couchbase.core.query.N1QLExpression.*; +import static org.springframework.data.couchbase.core.query.QueryCriteria.*; import java.util.Iterator; import java.util.Optional; @@ -33,11 +33,10 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; 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.expression.spel.standard.SpelExpressionParser; /** * @author Michael Reiche @@ -48,15 +47,14 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator context; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final CouchbaseQueryMethod queryMethod; private final CouchbaseConverter couchbaseConverter; private final String queryString; - private final SpelExpressionParser spelExpressionParser; public StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMethod queryMethod, - CouchbaseConverter couchbaseConverter, SpelExpressionParser spelExpressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) { + CouchbaseConverter couchbaseConverter, + ValueExpressionDelegate valueExpressionDelegate, NamedQueries namedQueries) { // AbstractQueryCreator needs a PartTree, so we give it a dummy one. // The resulting dummy criteria will not be included in the Query @@ -68,8 +66,7 @@ public StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMe this.context = couchbaseConverter.getMappingContext(); this.queryMethod = queryMethod; this.couchbaseConverter = couchbaseConverter; - this.spelExpressionParser = spelExpressionParser; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; final String namedQueryName = queryMethod.getNamedQueryName(); String qString; if (queryMethod.hasInlineN1qlQuery()) { @@ -133,7 +130,7 @@ protected QueryCriteria or(QueryCriteria base, QueryCriteria criteria) { @Override protected Query complete(QueryCriteria criteria, Sort sort) { // everything we need in the StringQuery such that we can doParse() later when we have the scope and collection - Query q = new StringQuery(queryMethod, queryString, evaluationContextProvider, accessor, spelExpressionParser) + Query q = new StringQuery(queryMethod, queryString, valueExpressionDelegate, accessor) .with(sort); return q; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/AwtPointInShapeEvaluator.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/AwtPointInShapeEvaluator.java index a17ddddb1..a61680228 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/AwtPointInShapeEvaluator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/AwtPointInShapeEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/GeoUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/GeoUtils.java index 4bf5df030..10738b360 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/GeoUtils.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/GeoUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java index ce720b23b..83d4d2ed6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java index 319f4373b..eac409ce5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,8 +16,15 @@ package org.springframework.data.couchbase.repository.query.support; -import static org.springframework.data.couchbase.core.query.N1QLExpression.*; -import static org.springframework.data.couchbase.core.support.TemplateUtils.*; +import static org.springframework.data.couchbase.core.query.N1QLExpression.count; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.meta; +import static org.springframework.data.couchbase.core.query.N1QLExpression.path; +import static org.springframework.data.couchbase.core.query.N1QLExpression.s; +import static org.springframework.data.couchbase.core.query.N1QLExpression.select; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS; +import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID; import java.util.ArrayList; import java.util.List; @@ -32,10 +39,12 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.query.CountFragment; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.Alias; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.TypeInformation; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; @@ -50,6 +59,7 @@ * @author Simon Baslé * @author Subhashni Balakrishnan * @author Mark Paluch + * @author Michael Reiche */ public class N1qlUtils { @@ -170,15 +180,23 @@ public static N1QLExpression createWhereFilterForEntity(N1QLExpression baseWhere // add part that filters on type key String typeKey = converter.getTypeKey(); String typeValue = entityInformation.getJavaType().getName(); - N1QLExpression typeSelector = i(typeKey).eq(s(typeValue)); + Alias alias = converter.getTypeAlias(TypeInformation.of(entityInformation.getJavaType())); + if (alias != null && alias.isPresent()) { + typeValue = alias.toString(); + } + N1QLExpression typeSelector = !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)) : null; if (baseWhereCriteria == null) { baseWhereCriteria = typeSelector; - } else { + } else if (typeSelector != null) { baseWhereCriteria = x("(" + baseWhereCriteria.toString() + ")").and(typeSelector); } return baseWhereCriteria; } + private static boolean empty(String s) { + return s == null || s.length() == 0; + } + /** * Given a common {@link PropertyPath}, returns the corresponding {@link PersistentPropertyPath} of * {@link CouchbasePersistentProperty} which will allow to discover alternative naming for fields. @@ -241,8 +259,13 @@ public static N1QLExpression[] createSort(Sort sort) { */ public static N1QLExpression createCountQueryForEntity(String bucketName, CouchbaseConverter converter, CouchbaseEntityInformation entityInformation) { - return select(count(x("*")).as(x(CountFragment.COUNT_ALIAS))).from(escapedBucket(bucketName)) - .where(createWhereFilterForEntity(null, converter, entityInformation)); + N1QLExpression entityFilter = createWhereFilterForEntity(null, converter, entityInformation); + N1QLExpression expression = select( + (count(x("*")).as(x(CountFragment.COUNT_ALIAS))).from(escapedBucket(bucketName))); + if (entityFilter == null) { + return expression; + } + return expression.where(entityFilter); } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/PointInShapeEvaluator.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/PointInShapeEvaluator.java index 55ef7fa0b..0769d09eb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/PointInShapeEvaluator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/PointInShapeEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java b/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java index d3294d38c..05f994976 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -104,6 +104,11 @@ private boolean querySettingsEquals(BasicQuery that) { return super.equals(that); } + @Override + public boolean isReadonly() { + return true; + } + /* * (non-Javadoc) */ diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java index dacbda52d..1118bffa0 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the original author or authors. + * Copyright 2011-2025 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. @@ -15,16 +15,19 @@ */ package org.springframework.data.couchbase.repository.support; +import static com.querydsl.apt.APTOptions.QUERYDSL_LOG_INFO; + import java.util.Collections; +import java.util.Set; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.lang.Nullable; import com.querydsl.apt.AbstractQuerydslProcessor; import com.querydsl.apt.Configuration; @@ -34,6 +37,7 @@ import com.querydsl.core.annotations.QueryEntities; import com.querydsl.core.annotations.QuerySupertype; import com.querydsl.core.annotations.QueryTransient; +import org.jspecify.annotations.Nullable; /** * Annotation processor to create Querydsl query types for QueryDsl annotated classes. @@ -51,7 +55,7 @@ public class CouchbaseAnnotationProcessor extends AbstractQuerydslProcessor { @Override protected Configuration createConfiguration(@Nullable RoundEnvironment roundEnv) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Running " + getClass().getSimpleName()); + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Running override createConfiguration() " + getClass().getSimpleName()); DefaultConfiguration configuration = new DefaultConfiguration(processingEnv, roundEnv, Collections.emptySet(), QueryEntities.class, Document.class, QuerySupertype.class, QueryEmbeddable.class, QueryEmbedded.class, @@ -60,4 +64,52 @@ protected Configuration createConfiguration(@Nullable RoundEnvironment roundEnv) return configuration; } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + setLogInfo(); + logInfo("Running override process() " + getClass().getSimpleName() +" isOver: "+roundEnv.processingOver() +" annotations: "+annotations.size()); + + if (roundEnv.processingOver() || annotations.size() == 0) { + return ALLOW_OTHER_PROCESSORS_TO_CLAIM_ANNOTATIONS; + } + + if (roundEnv.getRootElements() == null || roundEnv.getRootElements().isEmpty()) { + logInfo("No sources to process"); + return ALLOW_OTHER_PROCESSORS_TO_CLAIM_ANNOTATIONS; + } + + Configuration conf = createConfiguration(roundEnv); + try { + conf.getTypeMappings(); + } catch (NoClassDefFoundError cnfe ){ + logWarn( cnfe +" add a dependency on javax.inject:javax.inject to create querydsl classes"); + return ALLOW_OTHER_PROCESSORS_TO_CLAIM_ANNOTATIONS; + } + return super.process(annotations, roundEnv); + + } + + private boolean shouldLogInfo; + + private void setLogInfo() { + boolean hasProperty = processingEnv.getOptions().containsKey(QUERYDSL_LOG_INFO); + if (hasProperty) { + String val = processingEnv.getOptions().get(QUERYDSL_LOG_INFO); + shouldLogInfo = Boolean.parseBoolean(val); + } + } + + private void logInfo(String message) { + if (shouldLogInfo) { + System.out.println("[NOTE] "+message); // maven compiler swallows messages to messager + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message); + } + } + + private void logWarn(String message) { + System.err.println("[WARNING] "+message); // maven compiler swallows messages to messager + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message); + } } + diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index f93fde619..e8d8edcfb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. @@ -18,7 +18,9 @@ import java.lang.reflect.AnnotatedElement; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.couchbase.repository.Scope; @@ -35,7 +37,7 @@ * * @author Michael Reiche */ -public class CouchbaseRepositoryBase { +public abstract class CouchbaseRepositoryBase { /** * Contains information about the entity being used in this repository. @@ -82,9 +84,11 @@ String getId(S entity) { protected String getScope() { String fromAnnotation = OptionsBuilder.annotationString(Scope.class, CollectionIdentifier.DEFAULT_SCOPE, - new AnnotatedElement[] { getJavaType(), repositoryInterface }); + new AnnotatedElement[] { getJavaType(), getRepositoryInterface() }); String fromMetadata = crudMethodMetadata != null ? crudMethodMetadata.getScope() : null; - return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_SCOPE, fromMetadata, fromAnnotation); + PseudoArgs pa = getReactiveTemplate().getPseudoArgs(); + String fromThreadLocal = pa != null ? pa.getScope() : null; + return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_SCOPE, fromThreadLocal, fromMetadata, fromAnnotation); } /** @@ -96,12 +100,18 @@ protected String getScope() { * 1. repository.withCollection() */ protected String getCollection() { - String fromAnnotation = OptionsBuilder.annotationString(Collection.class, CollectionIdentifier.DEFAULT_COLLECTION, - new AnnotatedElement[] { getJavaType(), repositoryInterface }); + String fromAnnotation = OptionsBuilder.annotationString(Collection.class, + CollectionIdentifier.DEFAULT_COLLECTION, + new AnnotatedElement[] { getJavaType(), getRepositoryInterface() }); String fromMetadata = crudMethodMetadata != null ? crudMethodMetadata.getCollection() : null; - return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_COLLECTION, fromMetadata, fromAnnotation); + PseudoArgs pa = getReactiveTemplate().getPseudoArgs(); + String fromThreadLocal = pa != null ? pa.getCollection() : null; + return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_COLLECTION, fromThreadLocal, fromMetadata, + fromAnnotation); } + protected abstract ReactiveCouchbaseTemplate getReactiveTemplate(); + /** * Get the QueryScanConsistency from
* 1. The method annotation (method *could* be available from crudMethodMetadata)
@@ -132,4 +142,5 @@ QueryScanConsistency getQueryScanConsistency() { void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { this.crudMethodMetadata = crudMethodMetadata; } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index 610491c6a..8458ed239 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT; +import static org.springframework.data.querydsl.QuerydslUtils.*; import java.io.Serializable; import java.lang.reflect.Method; @@ -39,9 +39,10 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -118,7 +119,7 @@ public CouchbaseEntityInformation getEntityInformation(Class d * @return a new created repository. */ @Override - protected final Object getTargetRepository(final RepositoryInformation metadata) { + protected Object getTargetRepository(final RepositoryInformation metadata) { CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); @@ -142,8 +143,8 @@ protected final Class getRepositoryBaseClass(final RepositoryMetadata reposit @Override protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, - QueryMethodEvaluationContextProvider contextProvider) { - return Optional.of(new CouchbaseQueryLookupStrategy(contextProvider)); + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new CouchbaseQueryLookupStrategy(valueExpressionDelegate)); } /** @@ -151,10 +152,10 @@ protected Optional getQueryLookupStrategy(QueryLookupStrate */ private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy { - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; - public CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) { - this.evaluationContextProvider = evaluationContextProvider; + public CouchbaseQueryLookupStrategy(ValueExpressionDelegate valueExpressionDelegate) { + this.valueExpressionDelegate = new CachingValueExpressionDelegate(valueExpressionDelegate); } @Override @@ -166,11 +167,11 @@ public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadat CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); if (queryMethod.hasN1qlAnnotation()) { - return new StringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), - evaluationContextProvider, namedQueries); + return new StringBasedCouchbaseQuery(queryMethod, couchbaseOperations, + valueExpressionDelegate, namedQueries); } else { - return new PartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), - evaluationContextProvider); + return new PartTreeCouchbaseQuery(queryMethod, couchbaseOperations, + valueExpressionDelegate); } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java index b80d61782..95cf7f640 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java index ef6905c9a..3490d82a8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java index e6e913041..29c5b0967 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -34,7 +34,7 @@ import org.springframework.data.couchbase.repository.Scope; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -55,7 +55,7 @@ * @author Jens Schauder * @author Michael Reiche */ -class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, BeanClassLoaderAware { +public class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, BeanClassLoaderAware { private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java b/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java index 6bb2f96a3..4d1722bf5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index a60c5af6c..3bbe8ba0e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. @@ -102,7 +102,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl new DynamicInvocationHandler<>(target, options, (String) args[0], scope)); } - Class[] paramTypes = null; + Class[] paramTypes = new Class[0]; if (args != null) { // the CouchbaseRepository methods - save(entity) etc - will have a parameter type of Object instead of entityType // so change the paramType to match @@ -110,12 +110,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl .map(o -> o == null ? null : (o.getClass() == entityInformation.getJavaType() ? Object.class : o.getClass())) .toArray(Class[]::new); // the CouchbaseRepository methods - findById(id) etc - will have a parameter type of Object instead of ID - if (method.getName().endsWith("ById") && args.length == 1) { + // but deleteByAllId first parameter will be an iterable. + if (method.getName().endsWith("ById") && args.length == 1 && ! Iterable.class.isAssignableFrom(paramTypes[0]) ) { paramTypes[0] = Object.class; } } - Method theMethod = repositoryClass.getMethod(method.getName(), paramTypes); + Method theMethod = FindMethod.findMethod(repositoryClass, method.getName(), paramTypes); Object result; try { diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java index f7e733244..f174bcc1a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/FindMethod.java b/src/main/java/org/springframework/data/couchbase/repository/support/FindMethod.java new file mode 100644 index 000000000..22b37479e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/FindMethod.java @@ -0,0 +1,166 @@ +/* + * Copyright 2021-2025 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.couchbase.repository.support; + +import java.lang.reflect.Method; +import java.util.Vector; + +/** + * From jonskeet.uk as provided in https://groups.google.com/g/comp.lang.java.programmer/c/khq5GGXIzC4 + */ +public class FindMethod { + /** + * Finds the most specific applicable method + * + * @param source Class to find method in + * @param name Name of method to find + * @param parameterTypes Parameter types to search for + */ + public static Method findMethod(Class source, String name, Class[] parameterTypes) throws NoSuchMethodException { + return internalFind(source.getMethods(), name, parameterTypes); + } + + /** + * Finds the most specific applicable declared method + * + * @param source Class to find method in + * @param name Name of method to find + * @param parameterTypes Parameter types to search for + */ + public static Method findDeclaredMethod(Class source, String name, Class[] parameterTypes) + throws NoSuchMethodException { + return internalFind(source.getDeclaredMethods(), name, parameterTypes); + } + + /** + * Internal method to find the most specific applicable method + */ + private static Method internalFind(Method[] toTest, String name, Class[] parameterTypes) + throws NoSuchMethodException { + int l = parameterTypes.length; + + // First find the applicable methods + Vector applicableMethods = new Vector(); + for (int i = 0; i < toTest.length; i++) { + // Check the name matches + if (!toTest[i].getName().equals(name)) { + continue; + } + // Check the parameters match + Class[] params = toTest[i].getParameterTypes(); + if (params.length != l) { + continue; + } + int j; + for (j = 0; j < l; j++) { + if(params[j] == int.class && parameterTypes[j] == Integer.class ) + continue; + if(params[j] == long.class && parameterTypes[j] == Long.class ) + continue; + if(params[j] == double.class && parameterTypes[j] == Double.class ) + continue; + if(params[j] == boolean.class && parameterTypes[j] == Boolean.class ) + continue; + if (!params[j].isAssignableFrom(parameterTypes[j])) + break; + } + // If so, add it to the list + if (j == l) { + applicableMethods.add(toTest[i]); + } + } + + /* + * If we've got one or zero methods, we can finish + * the job now. + */ + int size = applicableMethods.size(); + if (size == 0) { + throw new NoSuchMethodException("No such method: " + name); + } + if (size == 1) { + return (Method) applicableMethods.elementAt(0); + } + + /* + * Now find the most specific method. Do this in a very primitive + * way - check whether each method is maximally specific. If more + * than one method is maximally specific, we'll throw an exception. + * For a definition of maximally specific, see JLS section 15.11.2.2. + * + * I'm sure there are much quicker ways - and I could probably + * set the second loop to be from i+1 to size. I'd rather not though, + * until I'm sure... + */ + int maximallySpecific = -1; // Index of maximally specific method + for (int i = 0; i < size; i++) { + int j; + // In terms of the JLS, current is T + Method current = (Method) applicableMethods.elementAt(i); + Class[] currentParams = current.getParameterTypes(); + Class currentDeclarer = current.getDeclaringClass(); + for (j = 0; j < size; j++) { + if (i == j) { + continue; + } + // In terms of the JLS, test is U + Method test = (Method) applicableMethods.elementAt(j); + Class[] testParams = test.getParameterTypes(); + Class testDeclarer = test.getDeclaringClass(); + + // Check if T is a subclass of U, breaking if not + if (!testDeclarer.isAssignableFrom(currentDeclarer)) { + break; + } + + // Check if each parameter in T is a subclass of the + // equivalent parameter in U + int k; + for (k = 0; k < l; k++) { + if (!testParams[k].isAssignableFrom(currentParams[k])) { + break; + } + } + if (k != l) { + break; + } + } + // Maximally specific! + if (j == size) { + if (maximallySpecific != -1) { + // if one returns Iterable and the other returns List, use the List + if (size == 2) { + if (((Method) applicableMethods.elementAt(0)).getReturnType() == java.lang.Iterable.class + && ((Method) applicableMethods.elementAt(1)).getReturnType() == java.util.List.class) { + return (Method) applicableMethods.elementAt(1); + } else if (((Method) applicableMethods.elementAt(1)).getReturnType() == java.lang.Iterable.class + && ((Method) applicableMethods.elementAt(0)).getReturnType() == java.util.List.class) { + return (Method) applicableMethods.elementAt(0); + } + } + throw new NoSuchMethodException( + "Ambiguous method search - more " + "than one maximally specific method"); + } + maximallySpecific = i; + } + } + if (maximallySpecific == -1) { + throw new NoSuchMethodException("No maximally specific method."); + } + return (Method) applicableMethods.elementAt(maximallySpecific); + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/MappingCouchbaseEntityInformation.java b/src/main/java/org/springframework/data/couchbase/repository/support/MappingCouchbaseEntityInformation.java index d3050f6cb..aaa8e498d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/MappingCouchbaseEntityInformation.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/MappingCouchbaseEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java index 033ad1204..5964f6251 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java index c98b080e3..ef9cdc6c1 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java index eb3648332..2f8f98ad7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -34,8 +34,8 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -106,7 +106,7 @@ public CouchbaseEntityInformation getEntityInformation(Class d * @return a new created repository. */ @Override - protected final Object getTargetRepository(final RepositoryInformation metadata) { + protected Object getTargetRepository(final RepositoryInformation metadata) { ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); @@ -132,8 +132,8 @@ protected final Class getRepositoryBaseClass(final RepositoryMetadata reposit @Override protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, - QueryMethodEvaluationContextProvider contextProvider) { - return Optional.of(new ReactiveCouchbaseRepositoryFactory.CouchbaseQueryLookupStrategy(contextProvider)); + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new ReactiveCouchbaseRepositoryFactory.CouchbaseQueryLookupStrategy(valueExpressionDelegate)); } /** @@ -141,10 +141,10 @@ protected Optional getQueryLookupStrategy(QueryLookupStrate */ private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy { - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; - public CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) { - this.evaluationContextProvider = evaluationContextProvider; + public CouchbaseQueryLookupStrategy(ValueExpressionDelegate valueExpressionDelegate) { + this.valueExpressionDelegate = valueExpressionDelegate; } @Override @@ -156,11 +156,11 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, mappingContext); if (queryMethod.hasN1qlAnnotation()) { - return new ReactiveStringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), - evaluationContextProvider, namedQueries); + return new ReactiveStringBasedCouchbaseQuery(queryMethod, couchbaseOperations, + valueExpressionDelegate, namedQueries); } else { - return new ReactivePartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), - evaluationContextProvider); + return new ReactivePartTreeCouchbaseQuery(queryMethod, couchbaseOperations, + valueExpressionDelegate); } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactoryBean.java index 438f882ca..d7df9c770 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index 1ee9473b1..f71ea46cd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. @@ -23,8 +23,8 @@ import java.util.stream.Collectors; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; @@ -37,7 +37,6 @@ import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.util.ReflectionUtils; /** * Repository base implementation for Couchbase. @@ -71,13 +70,25 @@ public SimpleCouchbaseRepository(CouchbaseEntityInformation entityInf @Override @SuppressWarnings("unchecked") public S save(S entity) { - return operations.save(entity, getScope(), getCollection()); + String scopeName = getScope(); + String collectionName = getCollection(); + // clear out the PseudoArgs here as whatever is called by operations.save() could be in a different thread. + // note that this will also clear out Options, but that's ok as any options would not work + // with all of insert/upsert/replace. If Options are needed, use template.insertById/upsertById/replaceById + getReactiveTemplate().setPseudoArgs(null); + return operations.save(entity, scopeName, collectionName); } @Override public Iterable saveAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); - return Streamable.of(entities).stream().map((e) -> save(e)).collect(StreamUtils.toUnmodifiableList()); + String scopeName = getScope(); + String collectionName = getCollection(); + // clear out the PseudoArgs here as whatever is called by operations.save() could be in a different thread. + // note that this will also clear out Options, but that's ok as any options would not work + // with all of insert/upsert/replace. If Options are needed, use template.insertById/upsertById/replaceById + getReactiveTemplate().setPseudoArgs(null); + return Streamable.of(entities).stream().map((e) -> operations.save(e,scopeName, collectionName)).collect(StreamUtils.toUnmodifiableList()); } @Override @@ -111,7 +122,7 @@ public void deleteById(ID id) { @Override public void delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); - operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(getId(entity)); + operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).oneEntity(entity); } @Override @@ -125,7 +136,7 @@ public void deleteAllById(Iterable ids) { public void deleteAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) - .all(Streamable.of(entities).map(this::getId).toList()); + .allEntities((Collection)Streamable.of(entities).toList()); } @Override @@ -177,4 +188,8 @@ public CouchbaseOperations getOperations() { return operations; } + @Override + protected ReactiveCouchbaseTemplate getReactiveTemplate() { + return ((CouchbaseTemplate) getOperations()).reactive(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 658843563..7d7eebfc5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -26,15 +26,13 @@ import org.reactivestreams.Publisher; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.domain.Sort; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; /** * Reactive repository base implementation for Couchbase. @@ -76,7 +74,13 @@ public Flux findAll(Sort sort) { @SuppressWarnings("unchecked") @Override public Mono save(S entity) { - return save(entity, getScope(), getCollection()); + String scopeName = getScope(); + String collectionName = getCollection(); + // clear out the PseudoArgs here as whatever is called by operations.save() could be in a different thread. + // note that this will also clear out Options, but that's ok as any options would not work + // with all of insert/upsert/replace. If Options are needed, use template.insertById/upsertById/replaceById + getReactiveTemplate().setPseudoArgs(null); + return operations.save(entity, scopeName, collectionName); } @Override @@ -84,6 +88,10 @@ public Flux saveAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); String scope = getScope(); String collection = getCollection(); + // clear out the PseudoArgs here as whatever is called by operations.save() could be in a different thread. + // note that this will also clear out Options, but that's ok as any options would not work + // with all of insert/upsert/replace. If Options are needed, use template.insertById/upsertById/replaceById + getReactiveTemplate().setPseudoArgs(null); return Flux.fromIterable(entities).flatMap(e -> save(e, scope, collection)); } @@ -92,6 +100,10 @@ public Flux saveAll(Publisher entityStream) { Assert.notNull(entityStream, "The given Iterable of entities must not be null!"); String scope = getScope(); String collection = getCollection(); + // clear out the PseudoArgs here as whatever is called by operations.save() could be in a different thread. + // note that this will also clear out Options, but that's ok as any options would not work + // with all of insert/upsert/replace. If Options are needed, use template.insertById/upsertById/replaceById + getReactiveTemplate().setPseudoArgs(null); return Flux.from(entityStream).flatMap(e -> save(e, scope, collection)); } @@ -227,4 +239,9 @@ public ReactiveCouchbaseOperations getOperations() { return operations; } + @Override + protected ReactiveCouchbaseTemplate getReactiveTemplate() { + return (ReactiveCouchbaseTemplate) getOperations(); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java index e5105a872..667ea90e7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -26,12 +26,13 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mysema.commons.lang.CloseableIterator; import com.mysema.commons.lang.EmptyCloseableIterator; @@ -42,9 +43,11 @@ import com.querydsl.core.types.Expression; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; +import org.springframework.util.StringUtils; /** * @author Michael Reiche + * @author Tigran Babloyan */ public class SpringDataCouchbaseQuery extends SpringDataCouchbaseQuerySupport> implements Fetchable { @@ -61,7 +64,7 @@ public class SpringDataCouchbaseQuery extends SpringDataCouchbaseQuerySupport * @param type must not be {@literal null}. */ public SpringDataCouchbaseQuery(CouchbaseOperations operations, Class type) { - this(operations, type, DEFAULT_COLLECTION); + this(operations, type, OptionsBuilder.getCollectionFrom(type)); } /** @@ -92,7 +95,7 @@ public SpringDataCouchbaseQuery(CouchbaseOperations operations, Class) couchbaseOperations.findByQuery(domainType) - .as(resultType1).inCollection(collectionName); + .as(resultType1).inCollection(StringUtils.hasText(collectionName) ? collectionName : DEFAULT_COLLECTION); } /* diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java index f02d55a36..805583e4f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -24,8 +24,8 @@ import com.querydsl.core.support.QueryMixin; import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.couchbase.document.AbstractCouchbaseQueryDSL; -import com.querydsl.couchbase.document.CouchbaseDocumentSerializer; +import org.springframework.data.couchbase.querydsl.document.AbstractCouchbaseQueryDSL; +import org.springframework.data.couchbase.querydsl.document.CouchbaseDocumentSerializer; /** * @author Michael Reiche diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java index 27edc2e3d..7971e1fb8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the original author or authors. + * Copyright 2011-2025 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. @@ -21,14 +21,14 @@ import java.util.Set; import java.util.regex.Pattern; -import com.querydsl.couchbase.document.CouchbaseDocumentSerializer; +import org.springframework.data.couchbase.querydsl.document.CouchbaseDocumentSerializer; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/Util.java b/src/main/java/org/springframework/data/couchbase/repository/support/Util.java index bb366c0f0..873379b2c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/Util.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/Util.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java b/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java index 05dca838f..60654db7d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java index 0a5625fb6..205691d89 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/package-info.java b/src/main/java/org/springframework/data/couchbase/repository/support/package-info.java index ec2996951..59123d022 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/package-info.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java index 301b97724..f37a072fd 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -30,7 +31,7 @@ import org.springframework.data.couchbase.transaction.error.TransactionRollbackRequestedException; import org.springframework.data.couchbase.transaction.error.TransactionSystemAmbiguousException; import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.ReactiveTransaction; import org.springframework.transaction.TransactionDefinition; @@ -89,6 +90,7 @@ public T execute(TransactionDefinition definition, TransactionCallback ca @Stability.Internal Flux executeReactive(TransactionDefinition definition, org.springframework.transaction.reactive.TransactionCallback callback) { + final CouchbaseResourceHolder couchbaseResourceHolder = new CouchbaseResourceHolder(null, getSecurityContext()); // caller's resources return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMapMany(isInTransaction -> { boolean isInExistingTransaction = isInTransaction.isPresent(); boolean createNewTransaction = handlePropagation(definition, isInExistingTransaction); @@ -100,17 +102,20 @@ Flux executeReactive(TransactionDefinition definition, } else { return Mono.error(new UnsupportedOperationException("Unsupported operation")); } - }); + }).contextWrite( // set CouchbaseResourceHolder containing caller's SecurityContext + ctx -> ctx.put(CouchbaseResourceHolder.class, couchbaseResourceHolder)); } private T executeNewTransaction(TransactionCallback callback) { final AtomicReference execResult = new AtomicReference<>(); + final CouchbaseResourceHolder couchbaseResourceHolder = new CouchbaseResourceHolder(null, getSecurityContext()); // Each of these transactions will block one thread on the underlying SDK's transactions scheduler. This // scheduler is effectively unlimited, but this can still potentially lead to high thread usage by the application. // If this is an issue then users need to instead use the standard Couchbase reactive transactions SDK. try { TransactionResult ignored = couchbaseClientFactory.getCluster().transactions().run(ctx -> { + setSecurityContext(couchbaseResourceHolder.getSecurityContext()); // set the security context for the transaction CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(ctx, true, false, false, true, null); T res = callback.doInTransaction(status); @@ -173,12 +178,16 @@ public boolean isCompleted() { } }; - return Flux.from(callback.doInTransaction(status)).doOnNext(v -> out.add(v)).then(Mono.defer(() -> { - if (status.isRollbackOnly()) { - return Mono.error(new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set")); - } - return Mono.empty(); - })); + // Get caller's resources, set SecurityContext for the transaction + return CouchbaseResourceOwner.get().map(cbrh -> cbrh.map( c -> setSecurityContext(c.getSecurityContext()))) + .flatMap(ignore -> Flux.from(callback.doInTransaction(status)).doOnNext(v -> out.add(v)) + .then(Mono.defer(() -> { + if (status.isRollbackOnly()) { + return Mono.error(new TransactionRollbackRequestedException( + "TransactionStatus.isRollbackOnly() is set")); + } + return Mono.empty(); + }))); }); }, this.options).thenMany(Flux.defer(() -> Flux.fromIterable(out))).onErrorMap(ex -> { @@ -288,4 +297,27 @@ public void rollback(TransactionStatus ignored) throws TransactionException { throw new UnsupportedOperationException( "Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); } + + static private Object getSecurityContext() { + try { + Class securityContextHolderClass = Class + .forName("org.springframework.security.core.context.SecurityContextHolder"); + return securityContextHolderClass.getMethod("getContext").invoke(null); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException cnfe) { + } + return null; + } + + static private S setSecurityContext(S sc) { + try { + Class securityContextHolder = Class.forName("org.springframework.security.core.context.SecurityContext"); + Class securityContextHolderClass = Class + .forName("org.springframework.security.core.context.SecurityContextHolder"); + securityContextHolderClass.getMethod("setContext", new Class[] { securityContextHolder }).invoke(null, sc); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException cnfe) {} + return sc; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java index 58f920118..6f5bb9eb3 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. @@ -18,7 +18,7 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import com.couchbase.client.core.annotation.Stability; @@ -34,6 +34,8 @@ public class CouchbaseResourceHolder extends ResourceHolderSupport { private @Nullable CoreTransactionAttemptContext core; // which holds the atr + private @Nullable Object securityContext; // SecurityContext. We don't have the class. + Map getResultMap = new HashMap<>(); /** @@ -42,7 +44,17 @@ public class CouchbaseResourceHolder extends ResourceHolderSupport { * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. */ public CouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) { + this(core, null); + } + + /** + * Create a new {@link CouchbaseResourceHolder} for a given {@link CoreTransactionAttemptContext session}. + * + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + public CouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core, @Nullable Object securityContext) { this.core = core; + this.securityContext = securityContext; } /** @@ -53,6 +65,14 @@ public CoreTransactionAttemptContext getCore() { return core; } + /** + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + @Nullable + public Object getSecurityContext() { + return securityContext; + } + public Object transactionResultHolder(Object holder, Object o) { getResultMap.put(System.identityHashCode(o), holder); return holder; diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceOwner.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceOwner.java new file mode 100644 index 000000000..8be143e67 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceOwner.java @@ -0,0 +1,41 @@ +package org.springframework.data.couchbase.transaction; + +import reactor.core.publisher.Mono; + +import java.util.Optional; + +import com.couchbase.client.core.annotation.Stability.Internal; + +@Internal +public class CouchbaseResourceOwner { + private static final ThreadLocal marker = new ThreadLocal(); + + public CouchbaseResourceOwner() {} + + public static void set(CouchbaseResourceHolder toInject) { + if (marker.get() != null) { + throw new IllegalStateException( + "Trying to set resource holder when already inside a transaction - likely an internal bug, please report it"); + } else { + marker.set(toInject); + } + } + + public static void clear() { + marker.remove(); + } + + public static Mono> get() { + return Mono.deferContextual((ctx) -> { + CouchbaseResourceHolder fromThreadLocal = marker.get(); + CouchbaseResourceHolder fromReactive = ctx.hasKey(CouchbaseResourceHolder.class) + ? ctx.get(CouchbaseResourceHolder.class) + : null; + if (fromThreadLocal != null) { + return Mono.just(Optional.of(fromThreadLocal)); + } else { + return fromReactive != null ? Mono.just(Optional.of(fromReactive)) : Mono.just(Optional.empty()); + } + }); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java index 1554e01ab..71b81a989 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java index 79d9df5c4..35be17d84 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. @@ -20,7 +20,7 @@ import com.couchbase.client.core.annotation.Stability; import org.aopalliance.intercept.MethodInterceptor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.TransactionManager; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttributeSource; diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java index d782dd478..cd619f345 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -24,27 +24,29 @@ */ public class CouchbaseTransactionStatus extends DefaultTransactionStatus { - /** - * Create a new {@code DefaultTransactionStatus} instance. - * - * @param transaction underlying transaction object that can hold state - * for the internal transaction implementation - * @param newTransaction if the transaction is new, otherwise participating - * in an existing transaction - * @param newSynchronization if a new transaction synchronization has been - * opened for the given transaction - * @param readOnly whether the transaction is marked as read-only - * @param debug should debug logging be enabled for the handling of this transaction? - * Caching it in here can prevent repeated calls to ask the logging system whether - * debug logging should be enabled. - * @param suspendedResources a holder for resources that have been suspended - */ - public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources) { - super(transaction, - newTransaction, - newSynchronization, - readOnly, - debug, - suspendedResources); - } + /** + * Create a new {@code DefaultTransactionStatus} instance. + * + * @param transaction underlying transaction object that can hold state + * for the internal transaction implementation + * @param newTransaction if the transaction is new, otherwise participating + * in an existing transaction + * @param newSynchronization if a new transaction synchronization has been + * opened for the given transaction + * @param readOnly whether the transaction is marked as read-only + * @param debug should debug logging be enabled for the handling of this transaction? + * Caching it in here can prevent repeated calls to ask the logging system whether + * debug logging should be enabled. + * @param suspendedResources a holder for resources that have been suspended + */ + public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources) { + super(null, + transaction, + newTransaction, + newSynchronization, + false, + readOnly, + debug, + suspendedResources); + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java index 82394b23f..8130573ac 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java index 446f4d0e9..20b797270 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java index dc64817fb..131da19d2 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java index 63b6077c9..f73e0f4c9 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java index f76f8f50c..0fa0c2faf 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java index 98dd9900b..b13cbfea8 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 07555d015..41969214e 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Couchbase 5.1 M2 (2023.0.0) +Spring Data Couchbase 6.0 M4 (2025.1.0) Copyright (c) [2013-2019] Couchbase / Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -28,6 +28,24 @@ conditions of the subcomponent's license, as noted in the LICENSE file. + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/springframework/data/couchbase/cache/CacheUser.java b/src/test/java/org/springframework/data/couchbase/cache/CacheUser.java index 3ca78dbc6..1e5d8a95d 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CacheUser.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CacheUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -26,9 +26,11 @@ */ class CacheUser implements Serializable { // private static final long serialVersionUID = 8817717605659870262L; - String firstname; - String lastname; - String id; + String firstname; // must have getter/setter for Serialize/Deserialize + String lastname; // must have getter/setter for Serialize/Deserialize + String id; // must have getter/setter for Serialize/Deserialize + + public CacheUser() {}; public CacheUser(String id, String firstname, String lastname) { this.id = id; @@ -40,6 +42,25 @@ public String getId() { return id; } + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public void setId(String id) { + this.id = id; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } // define equals for assertEquals() public boolean equals(Object o) { if (o == null) { diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index 10ad82aa0..f0153fb49 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -24,10 +24,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.data.couchbase.domain.Config; /** * CouchbaseCache tests Theses tests rely on a cb server running. @@ -35,10 +40,14 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED, missesCapabilities = { Capabilities.COLLECTIONS }) +@SpringJUnitConfig(Config.class) +@DirtiesContext class CouchbaseCacheCollectionIntegrationTests extends CollectionAwareIntegrationTests { volatile CouchbaseCache cache; + @Autowired CouchbaseTemplate couchbaseTemplate; + @BeforeEach @Override public void beforeEach() { @@ -59,6 +68,32 @@ void cachePutGet() { assertEquals(user2, cache.get(user2.getId()).get()); // get user2 } + @Test + void cacheGetValueLoaderWithClass() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + assertNull(cache.get(user1.getId(), CacheUser.class)); // was not put -> cacheMiss + assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1 + assertEquals(user1, cache.get(user1.getId(), () -> user1, CacheUser.class)); // already put, get user1 + + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user2.getId(), CacheUser.class)); // was not put -> cacheMiss + assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // put and get user2 + assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // already put, get user2 + } + + @Test + void cacheGetValueLoaderNoClass() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + assertNull(cache.get(user1.getId())); // was not put -> cacheMiss + assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1 + assertEquals(user1, cache.get(user1.getId(), () -> user1)); // already put, get user1 + + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user2.getId())); // was not put -> cacheMiss + assertEquals(user2, cache.get(user2.getId(), () -> user2)); // put and get user2 + assertEquals(user2, cache.get(user2.getId(), () -> user2)); // already put, get user2 + } + @Test void cacheEvict() { CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionTranscoderIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionTranscoderIntegrationTests.java new file mode 100644 index 000000000..c77d31608 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionTranscoderIntegrationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2022-2025 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.couchbase.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * CouchbaseCache tests Theses tests rely on a cb server running. + * + * @author Michael Reiche + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED, missesCapabilities = { Capabilities.COLLECTIONS }) +@SpringJUnitConfig(Config.class) +@DirtiesContext +class CouchbaseCacheCollectionTranscoderIntegrationTests extends CollectionAwareIntegrationTests { + + volatile CouchbaseCache cache; + + @Autowired CouchbaseTemplate couchbaseTemplate; + + @BeforeEach + @Override + public void beforeEach() { + super.beforeEach(); + cache = CouchbaseCacheManager.create(couchbaseTemplate.getCouchbaseClientFactory()).createCouchbaseCache( + "myCache", CouchbaseCacheConfiguration.defaultCacheConfig().collection("my_collection").valueTranscoder( + couchbaseTemplate.getCouchbaseClientFactory().getCluster().environment().transcoder())); + + cache.clear(); + } + + @Test + void cachePutGet() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user1.getId(), CacheUser.class)); // was not put -> cacheMiss + cache.put(user1.getId(), user1); // put user1 + cache.put(user2.getId(), user2); // put user2 + assertEquals(user1, cache.get(user1.getId(), CacheUser.class)); // get user1 + assertEquals(user2, cache.get(user2.getId(), CacheUser.class)); // get user2 + } + + @Test + void cacheGetValueLoaderWithClass() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + assertNull(cache.get(user1.getId(), CacheUser.class)); // was not put -> cacheMiss + assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1 + assertEquals(user1, cache.get(user1.getId(), () -> user1, CacheUser.class)); // already put, get user1 + + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user2.getId(), CacheUser.class)); // was not put -> cacheMiss + assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // put and get user2 + assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // already put, get user2 + } + + @Test + void cacheGetValueLoaderNoClass() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + assertNull(cache.get(user1.getId())); // was not put -> cacheMiss + assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1 + assertEquals(user1, cache.get(user1.getId(), () -> user1, CacheUser.class)); // already put, get user1, needs class + + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user2.getId())); // was not put -> cacheMiss + assertEquals(user2, cache.get(user2.getId(), () -> user2)); // put and get user2 + assertEquals(user2, cache.get(user2.getId(), () -> user2 , CacheUser.class)); // already put, get user2, needs class + } + + @Test + void cacheEvict() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + cache.put(user1.getId(), user1); // put user1 + cache.put(user2.getId(), user2); // put user2 + cache.evict(user1.getId()); // evict user1 + assertNull(cache.get(user1.getId(), CacheUser.class)); // get user1 -> not present + assertEquals(user2, cache.get(user2.getId(), CacheUser.class)); // get user2 -> present + } + + @Test + void cacheClear() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + cache.put(user1.getId(), user1); // put user1 + cache.put(user2.getId(), user2); // put user2 + cache.clear(); + assertNull(cache.get(user1.getId(), CacheUser.class)); // get user1 -> not present + assertNull(cache.get(user2.getId(), CacheUser.class)); // get user2 -> not present + } + + @Test + void cacheHitMiss() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.get(user2.getId(), CacheUser.class)); // get user2 -> cacheMiss + // cache.put(user1.getId(), null); // JsonTranscoder cannot handle null + // assertNotNull(cache.get(user1.getId(),CacheUser.class)); // cacheHit null + // assertNull(cache.get(user1.getId(),CacheUser.class)); // fetch cached null + } + + @Test + void cachePutIfAbsent() { + CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); + CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2"); + assertNull(cache.putIfAbsent(user1.getId(), user1, CacheUser.class)); // should put user1, return null + assertEquals(user1, cache.putIfAbsent(user1.getId(), user2, CacheUser.class)); // should not put user2, should + // return user1 + assertEquals(user1, cache.get(user1.getId(), CacheUser.class)); // user1.getId() is still user1 + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index a365cac14..ea04bb39b 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -26,14 +26,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -43,11 +43,13 @@ */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) +@DirtiesContext class CouchbaseCacheIntegrationTests extends JavaIntegrationTests { volatile CouchbaseCache cache; @Autowired CouchbaseCacheManager cacheManager; // autowired not working @Autowired UserRepository userRepository; // autowired not working + @Autowired CouchbaseTemplate couchbaseTemplate; @BeforeEach @Override @@ -56,9 +58,6 @@ public void beforeEach() { cache = CouchbaseCacheManager.create(couchbaseTemplate.getCouchbaseClientFactory()).createCouchbaseCache("myCache", CouchbaseCacheConfiguration.defaultCacheConfig()); cache.clear(); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - cacheManager = ac.getBean(CouchbaseCacheManager.class); - userRepository = ac.getBean(UserRepository.class); } @AfterEach @@ -134,5 +133,4 @@ public void clearWithDelayOk() throws InterruptedException { @Test public void noOpt() {} - } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index e78365746..e6306e7d2 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -23,16 +23,23 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; -import com.couchbase.client.core.error.TimeoutException; -import com.couchbase.client.core.msg.kv.DurabilityLevel; -import com.couchbase.client.core.retry.RetryReason; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; @@ -41,21 +48,50 @@ import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById; import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById; -import org.springframework.data.couchbase.core.support.*; -import org.springframework.data.couchbase.domain.*; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.support.OneAndAllEntity; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithDurability; +import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.MutableUser; +import org.springframework.data.couchbase.domain.NaiveAuditorAware; +import org.springframework.data.couchbase.domain.PersonValue; +import org.springframework.data.couchbase.domain.Submission; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserAnnotated; +import org.springframework.data.couchbase.domain.UserAnnotated2; +import org.springframework.data.couchbase.domain.UserAnnotated3; +import org.springframework.data.couchbase.domain.UserAnnotatedDurability; +import org.springframework.data.couchbase.domain.UserAnnotatedDurabilityExpression; +import org.springframework.data.couchbase.domain.UserAnnotatedPersistTo; +import org.springframework.data.couchbase.domain.UserAnnotatedReplicateTo; +import org.springframework.data.couchbase.domain.UserAnnotatedTouchOnRead; +import org.springframework.data.couchbase.domain.UserNoAlias; +import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.msg.kv.MutationToken; +import com.couchbase.client.core.retry.RetryReason; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.kv.MutationState; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** - * KV tests Theses tests rely on a cb server running. + * KV test - these tests rely on a cb server running. * * @author Michael Nitschinger * @author Michael Reiche @@ -63,6 +99,8 @@ */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) +@DirtiesContext +@TestPropertySource(properties = { "valid.document.durability = MAJORITY" }) class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { @Autowired public CouchbaseTemplate couchbaseTemplate; @@ -71,6 +109,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { @BeforeEach @Override public void beforeEach() { + super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); @@ -81,11 +120,11 @@ public void beforeEach() { @Test void findByIdWithLock() { try { - User user = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user = new User("1", "user1", "user1"); couchbaseTemplate.upsertById(User.class).one(user); - User foundUser = couchbaseTemplate.findById(User.class).withLock(Duration.ofSeconds(2)).one(user.getId()); + User foundUser = couchbaseTemplate.findById(User.class).withLock(Duration.ofSeconds(5)).one(user.getId()); user.setVersion(foundUser.getVersion());// version will have changed assertEquals(user, foundUser); @@ -94,12 +133,40 @@ void findByIdWithLock() { ); assertTrue(exception.retryReasons().contains(RetryReason.KV_LOCKED), "should have been locked"); } finally { - sleepSecs(2); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + for(int i=0; i< 10; i++) { + sleepSecs(2); + try { + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + break; + } catch (Exception e) { + e.printStackTrace(); // gives IndexFailureException if the lock is still active + } + } } } + @Test + void findByIdNoAlias() { + String firstname = UUID.randomUUID().toString(); + try { + UserNoAlias user = new UserNoAlias("1", firstname, "user1"); + couchbaseTemplate.upsertById(UserNoAlias.class).one(user); + UserNoAlias foundUser = couchbaseTemplate.findById(UserNoAlias.class).one(user.getId()); + user.setVersion(foundUser.getVersion());// version will have changed + assertEquals(user, foundUser); + Query query = new Query(QueryCriteria.where(i("firstname")).eq(firstname)); + List queriedUsers = couchbaseTemplate.findByQuery(UserNoAlias.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query).all(); + assertEquals(1, queriedUsers.size(), "should have found exactly one"); + } finally { + Query query = new Query(QueryCriteria.where(i("firstname")).eq(firstname)); + List removeResult = couchbaseTemplate.removeByQuery(UserNoAlias.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query).all(); + assertEquals(1, removeResult.size(), "should have removed exactly one"); + } + } + @Test void findByIdWithExpiry() { try { @@ -122,7 +189,7 @@ void findByIdWithExpiry() { assertEquals(1, foundUsers.size(), "should have found exactly 1 user"); assertEquals(user2, foundUsers.iterator().next()); } finally { - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + //couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); } } @@ -283,7 +350,8 @@ void findProjectingPath() { @Test void withDurability() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - for (Class clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) { + for (Class clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedDurabilityExpression.class, + UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) { // insert, replace, upsert for (OneAndAllEntity operator : new OneAndAllEntity[]{couchbaseTemplate.insertById(clazz), couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz)}) { @@ -1039,6 +1107,8 @@ void insertById() { User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); User inserted = couchbaseTemplate.insertById(User.class).one(user); assertEquals(user, inserted); + User found = couchbaseTemplate.findById(User.class).one(user.getId()); + assertEquals(inserted, found); assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(User.class).one(user)); couchbaseTemplate.removeById(User.class).one(user.getId()); } @@ -1118,6 +1188,31 @@ void insertByIdWithAnnotatedDurability2() { couchbaseTemplate.removeById(UserAnnotatedDurability.class).one(user.getId()); } + @Test + void insertByIdWithAnnotatedDurabilityExpression() { + UserAnnotatedDurabilityExpression user = new UserAnnotatedDurabilityExpression(UUID.randomUUID().toString(), "firstname", "lastname"); + UserAnnotatedDurabilityExpression inserted = null; + + // occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests" + for (int i = 1; i != 5; i++) { + try { + inserted = couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class) + .one(user); + break; + } catch (Exception ofe) { + System.out.println("" + i + " caught: " + ofe); + couchbaseTemplate.removeByQuery(UserAnnotatedDurabilityExpression.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + if (i == 4) { + throw ofe; + } + sleepSecs(1); + } + } + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class).one(user)); + couchbaseTemplate.removeById(UserAnnotatedDurabilityExpression.class).one(user.getId()); + } + @Test void existsById() { String id = UUID.randomUUID().toString(); @@ -1167,10 +1262,114 @@ void saveAndFindImmutableById() { couchbaseTemplate.removeById(PersonValue.class).one(replaced.getId()); } + @Test + void rangeScan() { + String id = "A"; + String lower = null; + String upper = null; + for (int i = 0; i < 10; i++) { + if (lower == null) { + lower = "" + i; + } + User inserted = couchbaseTemplate.insertById(User.class).one(new User("" + i, "fn_" + i, "ln_" + i)); + upper = "" + i; + } + MutationToken mt = couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection() + .upsert(id, JsonObject.create().put("id", id)).mutationToken().get(); + Stream users = couchbaseTemplate.rangeScan(User.class).consistentWith(MutationState.from(mt)) + /*.withSort(ScanSort.ASCENDING)*/.rangeScan(lower, upper); + for (User u : users.toList()) { + System.err.print(u); + System.err.println(","); + assertTrue(u.getId().compareTo(lower) >= 0 && u.getId().compareTo(upper) <= 0); + couchbaseTemplate.removeById(User.class).one(u.getId()); + } + couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection().remove(id); + } + + @Test + void rangeScanId() { + String id = "A"; + String lower = null; + String upper = null; + for (int i = 0; i < 10; i++) { + if (lower == null) { + lower = "" + i; + } + User inserted = couchbaseTemplate.insertById(User.class).one(new User("" + i, "fn_" + i, "ln_" + i)); + upper = "" + i; + } + MutationToken mt = couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection() + .upsert(id, JsonObject.create().put("id", id)).mutationToken().get(); + Stream userIds = couchbaseTemplate.rangeScan(User.class).consistentWith(MutationState.from(mt)) + /*.withSort(ScanSort.ASCENDING)*/.rangeScanIds(lower, upper); + for (String userId : userIds.toList()) { + System.err.print(userId); + System.err.println(","); + assertTrue(userId.compareTo(lower) >= 0 && userId.compareTo(upper) <= 0); + couchbaseTemplate.removeById(User.class).one(userId); + } + couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection().remove(id); + + } + + @Test + @Disabled // it's finding _txn documents with source = a single 0 byte which fails to deserialize + void sampleScan() { + String id = "A"; + String lower = null; + String upper = null; + for (int i = 0; i < 10; i++) { + if (lower == null) { + lower = "" + i; + } + User inserted = couchbaseTemplate.insertById(User.class).one(new User("" + i, "fn_" + i, "ln_" + i)); + upper = "" + i; + } + MutationToken mt = couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection() + .upsert(id, JsonObject.create().put("id", id)).mutationToken().get(); + Stream users = couchbaseTemplate.rangeScan(User.class).consistentWith(MutationState.from(mt)) + /*.withSort(ScanSort.ASCENDING)*/.samplingScan(5l, (Long)null); + List usersList = users.toList(); + assertEquals(5, usersList.size(), "number in sample"); + for (User u : usersList) { + System.err.print(u); + System.err.println(","); + // assertTrue(u.getId().compareTo(lower) >= 0 && u.getId().compareTo(upper) <= 0); + //couchbaseTemplate.removeById(User.class).one(u.getId()); + } + couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection().remove(id); + } + + @Test + void sampleScanId() { + String id = "A"; + String lower = null; + String upper = null; + for (int i = 0; i < 10; i++) { + if (lower == null) { + lower = "" + i; + } + User inserted = couchbaseTemplate.insertById(User.class).one(new User("" + i, "fn_" + i, "ln_" + i)); + upper = "" + i; + } + MutationToken mt = couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection() + .upsert(id, JsonObject.create().put("id", id)).mutationToken().get(); + Stream userIds = couchbaseTemplate.rangeScan(User.class).consistentWith(MutationState.from(mt)) + /*.withSort(ScanSort.ASCENDING)*/.samplingScanIds(5l); + for (String userId : userIds.toList()) { + System.err.print(userId); + System.err.println(","); + //assertTrue(userId.compareTo(lower) >= 0 && userId.compareTo(upper) <= 0); + //couchbaseTemplate.removeById(User.class).one(userId); + } + couchbaseTemplate.getCouchbaseClientFactory().getDefaultCollection().remove(id); + + } + private void sleepSecs(int i) { try { Thread.sleep(i * 1000); } catch (InterruptedException ie) {} } - } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 29c4068b3..c698a7361 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -43,8 +43,8 @@ import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; -import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; +import org.springframework.data.couchbase.domain.ConfigScoped; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; @@ -57,6 +57,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; @@ -80,7 +81,8 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CollectionsConfig.class) +@SpringJUnitConfig(ConfigScoped.class) +@DirtiesContext class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired public CouchbaseTemplate couchbaseTemplate; @@ -698,7 +700,7 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class) + assertThrows(UnambiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class) .withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection).withOptions(options).all()); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 7caa04a75..c6c3705fd 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -26,8 +26,12 @@ import java.time.Instant; import java.time.temporal.TemporalAccessor; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -42,6 +46,7 @@ import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; +import org.springframework.data.couchbase.domain.PersonWithMaps; import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserJustLastName; @@ -55,6 +60,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -67,6 +73,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) +@DirtiesContext class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { @Autowired public CouchbaseTemplate couchbaseTemplate; @@ -127,6 +134,27 @@ void findByQueryAll() { } + + @Test + void findById() { + PersonWithMaps person1 = new PersonWithMaps(); + person1.setId(UUID.randomUUID().toString()); + Map> versions=new HashMap<>(); + Set versionSet = new HashSet<>(); + versionSet.add("1.0"); + versions.put("v",versionSet); + person1.setVersions(versions); + Map> releaseVersions = new HashMap<>(); + Map releaseMap = new HashMap<>(); + releaseMap.put("1","1"); + releaseVersions.put("1",releaseMap); + person1.setReleaseVersions(releaseVersions); + couchbaseTemplate.upsertById(PersonWithMaps.class).one(person1); + PersonWithMaps person2 = couchbaseTemplate.findById(PersonWithMaps.class).one(person1.getId()); + assertEquals(person1, person2); + couchbaseTemplate.removeById(PersonWithMaps.class).oneEntity(person1); + } + @Test void findByMatchingQuery() { User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); @@ -155,11 +183,11 @@ void findAssessmentDO() { ado = couchbaseTemplate.upsertById(AssessmentDO.class).one(ado); Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); - final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) + final List assementDOs = couchbaseTemplate.findByQuery(AssessmentDO.class) .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); - assertEquals("123", foundUsers.get(0).getId(), "id"); - assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); - assertEquals(ado, foundUsers.get(0)); + assertEquals("123", assementDOs.get(0).getId(), "id"); + assertEquals("44444444", assementDOs.get(0).getDocumentId(), "documentId"); + assertEquals(ado, assementDOs.get(0)); couchbaseTemplate.removeById(AssessmentDO.class).one(ado.getDocumentId()); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java index acde4e621..6fcfda15e 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -24,15 +24,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMapper; import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -45,6 +44,7 @@ */ @SpringJUnitConfig(CustomTypeKeyIntegrationTests.Config.class) @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@DirtiesContext public class CustomTypeKeyIntegrationTests extends ClusterAwareIntegrationTests { private static final String CUSTOM_TYPE_KEY = "javaClass"; @@ -70,43 +70,11 @@ void saveSimpleEntityCorrectlyWithDifferentTypeKey() { operations.removeById(User.class).one(user.getId()); } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - + static class Config extends org.springframework.data.couchbase.domain.Config { @Override public String typeKey() { return CUSTOM_TYPE_KEY; } - } } diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateConcurrencyTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateConcurrencyTests.java new file mode 100644 index 000000000..296129366 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateConcurrencyTests.java @@ -0,0 +1,64 @@ +package org.springframework.data.couchbase.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.Semaphore; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +@DirtiesContext +public class ReactiveCouchbaseTemplateConcurrencyTests { + + @Autowired public CouchbaseTemplate couchbaseTemplate; + + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + + @Test + public void shouldStoreArgsForLocalThread() throws InterruptedException { + // These will consume any args set on the current thread + PseudoArgs args1 = new PseudoArgs<>(reactiveCouchbaseTemplate, "aScope", "aCollection", null, Object.class); + PseudoArgs args2 = new PseudoArgs<>(reactiveCouchbaseTemplate, "aScope", "aCollection", null, Object.class); + + // Store args1 on this thread + reactiveCouchbaseTemplate.setPseudoArgs(args1); + + final PseudoArgs[] threadArgs = {null}; + + Semaphore awaitingArgs1 = new Semaphore(0); + Semaphore checkingArgs2 = new Semaphore(0); + + Thread t = new Thread(() -> { + // Store args2 on separate thread + reactiveCouchbaseTemplate.setPseudoArgs(args2); + awaitingArgs1.release(); + try { + // Wait to check args2 + checkingArgs2.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + threadArgs[0] = reactiveCouchbaseTemplate.getPseudoArgs(); + }); + t.start(); + + // Wait for separate thread to have set args2 + awaitingArgs1.acquire(); + + assertEquals(args1, reactiveCouchbaseTemplate.getPseudoArgs()); + checkingArgs2.release(); + t.join(); + + assertEquals(args2, threadArgs[0]); + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index e1aa3e349..e178dafc4 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -28,15 +28,19 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; -import com.couchbase.client.core.error.TimeoutException; -import com.couchbase.client.core.msg.kv.DurabilityLevel; -import com.couchbase.client.core.retry.RetryReason; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.OptimisticLockingFailureException; @@ -47,14 +51,31 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; -import org.springframework.data.couchbase.domain.*; +import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.MutableUser; +import org.springframework.data.couchbase.domain.PersonValue; +import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserAnnotated; +import org.springframework.data.couchbase.domain.UserAnnotated2; +import org.springframework.data.couchbase.domain.UserAnnotated3; +import org.springframework.data.couchbase.domain.UserAnnotatedDurability; +import org.springframework.data.couchbase.domain.UserAnnotatedPersistTo; +import org.springframework.data.couchbase.domain.UserAnnotatedReplicateTo; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.retry.RetryReason; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.query.QueryScanConsistency; /** * KV tests Theses tests rely on a cb server running. @@ -65,6 +86,7 @@ */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) +@DirtiesContext class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { @Autowired public CouchbaseTemplate couchbaseTemplate; @@ -120,7 +142,7 @@ void findByIdWithLock() { reactiveCouchbaseTemplate.upsertById(User.class).one(user).block(); - User foundUser = reactiveCouchbaseTemplate.findById(User.class).withLock(Duration.ofSeconds(2)) + User foundUser = reactiveCouchbaseTemplate.findById(User.class).withLock(Duration.ofSeconds(4)) .one(user.getId()).block(); user.setVersion(foundUser.getVersion());// version will have changed assertEquals(user, foundUser); @@ -130,7 +152,7 @@ void findByIdWithLock() { ); assertTrue(exception.retryReasons().contains(RetryReason.KV_LOCKED), "should have been locked"); } finally { - sleepSecs(2); + sleepSecs(5); reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all().collectList().block(); } diff --git a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java index 8663dc6b0..4bbe1db33 100644 --- a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java index 87dd2edd2..87ded75f9 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. @@ -23,17 +23,19 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.mock.env.MockPropertySource; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @SpringJUnitConfig -@TestPropertySource(properties = { "valid.document.expiry = 10", "invalid.document.expiry = abc" }) +@TestPropertySource(properties = { "valid.document.expiry = 10", "invalid.document.expiry = abc", + "invalid.document.durability = some", "valid.document.durability = MAJORITY" }) public class BasicCouchbasePersistentEntityTests { @Autowired ConfigurableEnvironment environment; @@ -41,7 +43,7 @@ public class BasicCouchbasePersistentEntityTests { @Test void testNoExpiryByDefault() { CouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(DefaultExpiry.class)); + TypeInformation.of(DefaultExpiry.class)); assertThat(entity.getExpiryDuration().getSeconds()).isEqualTo(0); } @@ -49,7 +51,7 @@ void testNoExpiryByDefault() { @Test void testDefaultExpiryUnitIsSeconds() { CouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(DefaultExpiryUnit.class)); + TypeInformation.of(DefaultExpiryUnit.class)); assertThat(entity.getExpiryDuration().getSeconds()).isEqualTo(78); } @@ -57,14 +59,14 @@ void testDefaultExpiryUnitIsSeconds() { @Test void testLargeExpiry30DaysStillInSeconds() { CouchbasePersistentEntity entityUnder = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(LimitDaysExpiry.class)); + TypeInformation.of(LimitDaysExpiry.class)); assertThat(entityUnder.getExpiryDuration().getSeconds()).isEqualTo(30 * 24 * 60 * 60); } @Test void testLargeExpiry31DaysIsConvertedToUnixUtcTime() { CouchbasePersistentEntity entityOver = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(OverLimitDaysExpiry.class)); + TypeInformation.of(OverLimitDaysExpiry.class)); int expiryOver = (int) entityOver.getExpiry(); Calendar expected = Calendar.getInstance(TimeZone.getTimeZone("UTC")); @@ -84,7 +86,7 @@ void testLargeExpiry31DaysIsConvertedToUnixUtcTime() { @Test void testLargeExpiryExpression31DaysIsConvertedToUnixUtcTime() { BasicCouchbasePersistentEntity entityOver = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(OverLimitDaysExpiryExpression.class)); + TypeInformation.of(OverLimitDaysExpiryExpression.class)); entityOver.setEnvironment(environment); int expiryOver = (int) entityOver.getExpiry(); @@ -105,7 +107,7 @@ void testLargeExpiryExpression31DaysIsConvertedToUnixUtcTime() { @Test void testLargeExpiry31DaysInSecondsIsConvertedToUnixUtcTime() { CouchbasePersistentEntity entityOver = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(OverLimitSecondsExpiry.class)); + TypeInformation.of(OverLimitSecondsExpiry.class)); int expiryOver = (int) entityOver.getExpiry(); Calendar expected = Calendar.getInstance(TimeZone.getTimeZone("UTC")); @@ -180,9 +182,37 @@ void doesNotAllowUseExpiryAndExpressionSimultaneously() { () -> getBasicCouchbasePersistentEntity(ExpiryAndExpression.class).getExpiry()); } + @Test + void doesNotAllowUseDurabilityAndExpressionSimultaneously() { + assertThrows(IllegalArgumentException.class, + () -> getBasicCouchbasePersistentEntity(DurabilityAndExpression.class).getDurabilityLevel()); + } + + @Test + void failsIfDurabilityExpressionMissesRequiredProperty() { + assertThrows(IllegalArgumentException.class, + () -> getBasicCouchbasePersistentEntity(DurabilityWithMissingProperty.class).getDurabilityLevel()); + } + + @Test + void doesNotAllowUseDurabilityFromInvalidExpression() { + assertThrows(IllegalArgumentException.class, + () -> getBasicCouchbasePersistentEntity(DurabilityWithInvalidExpression.class).getDurabilityLevel()); + } + + @Test + void usesGetDurabilityExpression() { + assertThat(getBasicCouchbasePersistentEntity(ConstantDurabilityExpression.class).getDurabilityLevel()).isEqualTo(DurabilityLevel.MAJORITY); + } + + @Test + void usesGetDurabilityFromValidExpression() { + assertThat(getBasicCouchbasePersistentEntity(DurabilityWithValidExpression.class).getDurabilityLevel()).isEqualTo(DurabilityLevel.MAJORITY); + } + private BasicCouchbasePersistentEntity getBasicCouchbasePersistentEntity(Class clazz) { BasicCouchbasePersistentEntity basicCouchbasePersistentEntity = new BasicCouchbasePersistentEntity( - ClassTypeInformation.from(clazz)); + TypeInformation.of(clazz)); basicCouchbasePersistentEntity.setEnvironment(environment); return basicCouchbasePersistentEntity; } @@ -264,4 +294,34 @@ class ExpiryWithMissingProperty {} @Document(expiry = 10, expiryExpression = "10") class ExpiryAndExpression {} + /** + * Simple POJO to test that durability and durability expression cannot be used simultaneously + */ + @Document(durabilityLevel = DurabilityLevel.MAJORITY, durabilityExpression = "MAJORITY") + class DurabilityAndExpression {} + + /** + * Simple POJO to test durability expression logic failure to resolve property placeholder + */ + @Document(durabilityExpression = "${missing.durability}") + class DurabilityWithMissingProperty {} + + /** + * Simple POJO to test invalid durability expression + */ + @Document(durabilityExpression = "${invalid.document.durability}") + class DurabilityWithInvalidExpression {} + + /** + * Simple POJO to test constant expiry expression + */ + @Document(durabilityExpression = "MAJORITY") + class ConstantDurabilityExpression {} + + /** + * Simple POJO to test valid expiry expression by resolving simple property from environment + */ + @Document(durabilityExpression = "${valid.document.durability}") + class DurabilityWithValidExpression {} + } diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java index 787c0a789..fbe3ea977 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. @@ -27,7 +27,7 @@ import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.ReflectionUtils; /** @@ -48,7 +48,7 @@ public class BasicCouchbasePersistentPropertyTests { */ @BeforeEach void beforeEach() { - entity = new BasicCouchbasePersistentEntity<>(ClassTypeInformation.from(Beer.class)); + entity = new BasicCouchbasePersistentEntity<>(TypeInformation.of(Beer.class)); } /** @@ -72,7 +72,7 @@ void usesAnnotatedFieldName() { @Test void testSdkIdAnnotationEvaluatedAfterSpringIdAnnotationIsIgnored() { BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(Beer.class)); + TypeInformation.of(Beer.class)); Field springIdField = ReflectionUtils.findField(Beer.class, "springId"); CouchbasePersistentProperty springIdProperty = getPropertyFor(springIdField); @@ -91,7 +91,7 @@ class TestIdField { @Id private String springId; } BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(TestIdField.class)); + TypeInformation.of(TestIdField.class)); Field springIdField = ReflectionUtils.findField(TestIdField.class, "springId"); CouchbasePersistentProperty springIdProperty = getPropertyFor(springIdField); test.addPersistentProperty(springIdProperty); @@ -108,7 +108,7 @@ class TestIdField { Field idField = ReflectionUtils.findField(TestIdField.class, "id"); CouchbasePersistentProperty idProperty = getPropertyFor(idField); BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(TestIdField.class)); + TypeInformation.of(TestIdField.class)); test.addPersistentProperty(idProperty); assertThat(test.getIdProperty()).isEqualTo(idProperty); } @@ -122,7 +122,7 @@ class TestIdField { private String id; } BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(TestIdField.class)); + TypeInformation.of(TestIdField.class)); Field springIdField = ReflectionUtils.findField(TestIdField.class, "springId"); Field idField = ReflectionUtils.findField(TestIdField.class, "id"); CouchbasePersistentProperty idProperty = getPropertyFor(idField); @@ -148,7 +148,7 @@ class TestIdField { CouchbasePersistentProperty idProperty = getPropertyFor(idField); CouchbasePersistentProperty springIdProperty = getPropertyFor(springIdField); BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(TestIdField.class)); + TypeInformation.of(TestIdField.class)); test.addPersistentProperty(springIdProperty); assertThatExceptionOfType(MappingException.class).isThrownBy(() -> { test.addPersistentProperty(idProperty); @@ -168,7 +168,7 @@ class TestIdField { CouchbasePersistentProperty idProperty = getPropertyFor(idField); CouchbasePersistentProperty springIdProperty = getPropertyFor(springIdField); BasicCouchbasePersistentEntity test = new BasicCouchbasePersistentEntity<>( - ClassTypeInformation.from(TestIdField.class)); + TypeInformation.of(TestIdField.class)); test.addPersistentProperty(springIdProperty); assertThatExceptionOfType(MappingException.class).isThrownBy(() -> { test.addPersistentProperty(idProperty); @@ -183,7 +183,7 @@ class TestIdField { */ private CouchbasePersistentProperty getPropertyFor(Field field) { - ClassTypeInformation type = ClassTypeInformation.from(field.getDeclaringClass()); + TypeInformation type = TypeInformation.of(field.getDeclaringClass()); return new BasicCouchbasePersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT, PropertyNameFieldNamingStrategy.INSTANCE); diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java index 8741327f0..0a80135a5 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java index 548114923..f8c39c407 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2025 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. @@ -25,6 +25,7 @@ import java.math.BigInteger; import java.text.ChoiceFormat; import java.time.LocalDateTime; +import java.time.YearMonth; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; @@ -62,6 +63,7 @@ import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserNoAlias; import org.springframework.data.mapping.MappingException; /** @@ -74,10 +76,17 @@ public class MappingCouchbaseConverterTests { private static MappingCouchbaseConverter converter = new MappingCouchbaseConverter(); private static MappingCouchbaseConverter customConverter = (new Config()).mappingCouchbaseConverter(); + private static MappingCouchbaseConverter noTypeKeyConverter = (new Config(){ + @Override + public String typeKey() { + return ""; + } + }).mappingCouchbaseConverter(); static { converter.afterPropertiesSet(); customConverter.afterPropertiesSet(); + noTypeKeyConverter.afterPropertiesSet(); } @Test @@ -151,15 +160,52 @@ void readsString() { assertThat(converted.attr0).isEqualTo(source.get("attr0")); } + @Test + void writesStringNoTypeKey() { + CouchbaseDocument converted = new CouchbaseDocument(); + StringEntity entity = new StringEntity("foobar"); + + noTypeKeyConverter.write(entity, converted); + Map result = converted.export(); + assertThat(result.get("_class")).isEqualTo(null); + assertThat(result.get("attr0")).isEqualTo(entity.attr0); + assertThat(converted.getId()).isEqualTo(BaseEntity.ID); + } + + @Test + void readsStringNoTypeKey() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("attr0", "foobar"); + StringEntity converted = noTypeKeyConverter.read(StringEntity.class, source); + assertThat(converted.attr0).isEqualTo(source.get("attr0")); + } + + @Test + void writesNoTypeAlias() { + CouchbaseDocument converted = new CouchbaseDocument(); + UserNoAlias entity = new UserNoAlias(UUID.randomUUID().toString(), "first", "last"); + noTypeKeyConverter.write(entity, converted); + Map result = converted.export(); + assertThat(result.get("_class")).isEqualTo(null); + assertThat(converted.getId()).isEqualTo(entity.getId()); + } + + @Test + void readsNoTypeAlias() { + CouchbaseDocument document = new CouchbaseDocument("001"); + UserNoAlias user = noTypeKeyConverter.read(UserNoAlias.class, document); + assertThat(user.getId()).isEqualTo("001"); + } + @Test void writesBigInteger() { CouchbaseDocument converted = new CouchbaseDocument(); - BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345")); + BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345678901234567890123")); converter.write(entity, converted); Map result = converted.export(); assertThat(result.get("_class")).isEqualTo(entity.getClass().getName()); - assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString()); + assertThat(result.get("attr0")).isEqualTo(entity.attr0); assertThat(converted.getId()).isEqualTo(BaseEntity.ID); } @@ -167,21 +213,21 @@ void writesBigInteger() { void readsBigInteger() { CouchbaseDocument source = new CouchbaseDocument(); source.put("_class", BigIntegerEntity.class.getName()); - source.put("attr0", "12345"); + source.put("attr0", new BigInteger("12345678901234567890123")); BigIntegerEntity converted = converter.read(BigIntegerEntity.class, source); - assertThat(converted.attr0).isEqualTo(new BigInteger((String) source.get("attr0"))); + assertThat(converted.attr0).isEqualTo(source.get("attr0")); } @Test void writesBigDecimal() { CouchbaseDocument converted = new CouchbaseDocument(); - BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("123.45")); + BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("12345678901234567890123.45")); converter.write(entity, converted); Map result = converted.export(); assertThat(result.get("_class")).isEqualTo(entity.getClass().getName()); - assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString()); + assertThat(result.get("attr0")).isEqualTo(entity.attr0); assertThat(converted.getId()).isEqualTo(BaseEntity.ID); } @@ -189,10 +235,10 @@ void writesBigDecimal() { void readsBigDecimal() { CouchbaseDocument source = new CouchbaseDocument(); source.put("_class", BigDecimalEntity.class.getName()); - source.put("attr0", "123.45"); + source.put("attr0", new BigDecimal("12345678901234567890123.45")); BigDecimalEntity converted = converter.read(BigDecimalEntity.class, source); - assertThat(converted.attr0).isEqualTo(new BigDecimal((String) source.get("attr0"))); + assertThat(converted.attr0).isEqualTo(source.get("attr0")); } @Test @@ -288,6 +334,28 @@ void readsID() { assertThat(user.getId()).isEqualTo("001"); } + @Test + void writesYearMonth() { + CouchbaseDocument converted = new CouchbaseDocument(); + YearMonthEntity entity = new YearMonthEntity(YearMonth.parse("2023-04")); + + converter.write(entity, converted); + Map result = converted.export(); + assertThat(result.get("_class")).isEqualTo(entity.getClass().getName()); + assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString()); + assertThat(converted.getId()).isEqualTo(BaseEntity.ID); + } + + @Test + void readsYearMonth() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", YearMonthEntity.class.getName()); + source.put("attr0", "2023-04"); + + YearMonthEntity converted = converter.read(YearMonthEntity.class, source); + assertThat(converted.attr0).isEqualTo(YearMonth.parse((String) source.get("attr0"))); + } + @Test void writesUninitializedValues() { CouchbaseDocument converted = new CouchbaseDocument(); @@ -786,6 +854,15 @@ public BigIntegerEntity(BigInteger attr0) { } } + static class YearMonthEntity extends BaseEntity { + private YearMonth attr0; + + public YearMonthEntity(YearMonth attr0) { + this.attr0 = attr0; + } + } + + static class BigDecimalEntity extends BaseEntity { private BigDecimal attr0; diff --git a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java index 131c5f6e1..f699644ca 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -24,11 +24,16 @@ import static org.springframework.data.couchbase.core.query.QueryCriteria.where; import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.escapedBucket; +import java.math.BigInteger; import java.util.Arrays; import org.junit.jupiter.api.Test; import com.couchbase.client.java.json.JsonArray; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.domain.Config; /** * @author Mauro Monti @@ -85,7 +90,7 @@ void testNestedOrCriteria() { @Test void testNestedNotIn() { QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).and(i("country")).is("Austria")) - .and(where(i("state")).notIn(new String[] { "Alabama", "Florida" })); + .and(where(i("state")).notIn( "Alabama", "Florida" )); JsonArray parameters = JsonArray.create(); assertEquals(" ( (`name` = $1) or (`age` > $2 and `country` = $3)) and (not( (`state` in $4) ))", c.export(new int[1], parameters, null)); @@ -235,6 +240,10 @@ void testIsNotValued() { void testBetween() { QueryCriteria c = where(i("name")).between("Davis", "Gump"); assertEquals("`name` between \"Davis\" and \"Gump\"", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` between $1 and $2", c.export(new int[1], parameters, converter)); + assertEquals("Davis", parameters.get(0).toString()); + assertEquals("Gump", parameters.get(1).toString()); } @Test @@ -243,7 +252,37 @@ void testIn() { QueryCriteria c = where(i("name")).in((Object) args); // the first arg is an array assertEquals("`name` in [\"gump\",\"davis\"]", c.export()); JsonArray parameters = JsonArray.create(); - assertEquals("`name` in $1", c.export(new int[1], parameters, null)); + assertEquals("`name` in $1", c.export(new int[1], parameters, converter)); + assertEquals(arrayToString(args), parameters.get(0).toString()); + } + + @Test + void testInInteger() { + Integer[] args = new Integer[]{1, 2}; + QueryCriteria c = where(i("name")).in((Object) args); // the first arg is an array + assertEquals("`name` in [1,2]", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` in $1", c.export(new int[1], parameters, converter)); + assertEquals(arrayToString(args), parameters.get(0).toString()); + } + + @Test + void testInBigBoolean() { + Boolean[] args = new Boolean[]{true, false}; + QueryCriteria c = where(i("name")).in((Object) args); // the first arg is an array + assertEquals("`name` in ["+true+","+false+"]", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` in $1", c.export(new int[1], parameters, converter)); + assertEquals(arrayToString(args), parameters.get(0).toString()); + } + + @Test + void testInBigInteger() { + BigInteger[] args = new BigInteger[]{BigInteger.TEN, BigInteger.ONE}; + QueryCriteria c = where(i("name")).in((Object) args); // the first arg is an array + assertEquals("`name` in ["+BigInteger.TEN+","+BigInteger.ONE+"]", c.export(null, null, converter)); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` in $1", c.export(new int[1], parameters, converter)); assertEquals(arrayToString(args), parameters.get(0).toString()); } @@ -254,7 +293,7 @@ void testNotIn() { assertEquals("not( (`name` in [\"gump\",\"davis\"]) )", c.export()); // this tests creating parameters from the args. JsonArray parameters = JsonArray.create(); - assertEquals("not( (`name` in $1) )", c.export(new int[1], parameters, null)); + assertEquals("not( (`name` in $1) )", c.export(new int[1], parameters, converter)); assertEquals(arrayToString(args), parameters.get(0).toString()); } @@ -262,12 +301,22 @@ void testNotIn() { void testTrue() { QueryCriteria c = where(i("name")).TRUE(); assertEquals("`name` = true", c.export()); + + JsonArray parameters1 = JsonArray.create(); + QueryCriteria c1 = where(i("name")).is(true); + assertEquals("`name` = $1", c1.export(new int[1], parameters1, converter)); + assertEquals("true", parameters1.get(0).toString()); } @Test void testFalse() { QueryCriteria c = where(i("name")).FALSE(); assertEquals("`name` = false", c.export()); + + JsonArray parameters1 = JsonArray.create(); + QueryCriteria c1 = where(i("name")).is(false); + assertEquals("`name` = $1", c1.export(new int[1], parameters1, converter)); + assertEquals("false", parameters1.get(0).toString()); } @Test @@ -304,17 +353,29 @@ private String arrayToString(Object[] array) { sb.append(","); } first = false; - if (e instanceof Number) - sb.append(e); - else { - sb.append("\""); - sb.append(e); - sb.append("\""); - } + sb.append(convert(e)); } sb.append("]"); } return sb.toString(); } + private static Config config = new Config(); + private static CouchbaseMappingContext mappingContext; + static { + try { + mappingContext = config.couchbaseMappingContext(config.customConversions()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static MappingCouchbaseConverter converter = (new Config()).mappingCouchbaseConverter(mappingContext,(CouchbaseCustomConversions)config.customConversions()); + Object convert(Object e){ + Object o = converter.convertForWriteIfNeeded(e); + if(o instanceof String){ + return "\""+o+"\""; + } + return o; + } } diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 70ef366d5..6b9f280de 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -38,12 +38,13 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; -import org.springframework.data.couchbase.domain.CollectionsConfig; +import org.springframework.data.couchbase.domain.ConfigScoped; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -52,10 +53,12 @@ import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.domain.UserSubmissionProjected; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; @@ -78,7 +81,8 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CollectionsConfig.class) +@SpringJUnitConfig(ConfigScoped.class) +@DirtiesContext class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired public CouchbaseTemplate couchbaseTemplate; @@ -111,10 +115,12 @@ public void beforeEach() { // then do processing for this class couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) .inCollection(otherCollection).all(); @@ -524,7 +530,6 @@ public void upsertById() { // 10 @Test public void existsByIdOther() { // 1 - GetOptions options = GetOptions.getOptions().timeout(Duration.ofSeconds(10)); ExistsOptions existsOptions = ExistsOptions.existsOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowg")).block(); @@ -689,7 +694,7 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, + assertThrows(UnambiguousTimeoutException.class, () -> template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) .inCollection(otherCollection).withOptions(options).all().collectList().block()); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java index b55c7f2f2..8c2b1fcc0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java index d2d886198..465eaf028 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java index 09c5ce4b0..95d002de4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java index fd29e6b37..776671693 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.domain; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -37,8 +38,9 @@ public class AbstractingMappingCouchbaseConverter extends MappingCouchbaseConver */ public AbstractingMappingCouchbaseConverter( final MappingContext, CouchbasePersistentProperty> mappingContext, - final String typeKey) { - super(mappingContext, typeKey); + final String typeKey, + final CouchbaseCustomConversions couchbaseCustomConversions) { + super(mappingContext, typeKey, couchbaseCustomConversions); this.typeMapper = new AbstractingTypeMapper(typeKey); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java index 44a6e7343..8fafdabdd 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/Address.java b/src/test/java/org/springframework/data/couchbase/domain/Address.java index 940023c0b..6f39a5ac0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Address.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Address.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java index 45237517a..4afadee23 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AddressWithEncStreet.java b/src/test/java/org/springframework/data/couchbase/domain/AddressWithEncStreet.java index db94cb043..fb8a66ba1 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AddressWithEncStreet.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AddressWithEncStreet.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airline.java b/src/test/java/org/springframework/data/couchbase/domain/Airline.java index c2f95e036..64032a743 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airline.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airline.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.domain; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.couchbase.core.index.CompositeQueryIndex; import org.springframework.data.couchbase.core.index.QueryIndexed; import org.springframework.data.couchbase.core.mapping.Document; @@ -34,7 +34,7 @@ public class Airline extends ComparableEntity { String hqCountry; - @PersistenceConstructor + @PersistenceCreator public Airline(String id, String name, String hqCountry) { this.id = id; this.name = name; diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectioned.java b/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectioned.java new file mode 100644 index 000000000..5848fecd2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectioned.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2025 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.couchbase.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.couchbase.core.index.QueryIndexed; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.Scope; +import org.springframework.data.couchbase.util.CollectionAwareDefaultScopeIntegrationTests; + +@Document +@Collection(CollectionAwareDefaultScopeIntegrationTests.otherCollection) +@Scope(CollectionAwareDefaultScopeIntegrationTests.otherScope) +public class AirlineCollectioned extends ComparableEntity { + @Id String id; + + @QueryIndexed String name; + + String hqCountry; + + @PersistenceCreator + public AirlineCollectioned(String id, String name, String hqCountry) { + this.id = id; + this.name = name; + this.hqCountry = hqCountry; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getHqCountry() { + return hqCountry; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectionedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectionedRepository.java new file mode 100644 index 000000000..a3f3a6ab4 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirlineCollectionedRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2025 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.couchbase.domain; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +/** + * @author Michael Reiche + */ +@Repository +public interface AirlineCollectionedRepository extends CouchbaseRepository, + QuerydslPredicateExecutor, DynamicProxyable { +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java index 8d1e964b0..fb01833d9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -16,13 +16,15 @@ package org.springframework.data.couchbase.domain; +import java.util.Collection; import java.util.List; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.querydsl.QuerydslPredicateExecutor; -import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -39,4 +41,5 @@ public interface AirlineRepository extends CouchbaseRepository, @Query("select meta().id as _ID, meta().cas as _CAS, #{#n1ql.bucket}.* from #{#n1ql.bucket} where #{#n1ql.filter} and (name = $1)") List getByName_3x(@Param("airline_name") String airlineName); + Page findByHqCountryIn(@Param("hqCountry") Collection hqCountry, Pageable pageable); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index 76f235833..7e06af6e6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -20,7 +20,7 @@ import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; @@ -59,7 +59,7 @@ public class Airport extends ComparableEntity { public Airport() {} - @PersistenceConstructor + @PersistenceCreator public Airport(String key, String iata, String icao) { this.key = key; this.iata = iata; diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java index d2af6496b..cc9a6883b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. @@ -20,7 +20,7 @@ import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; @@ -61,7 +61,7 @@ public class AirportJsonValue extends ComparableEntity { public AirportJsonValue() {} - @PersistenceConstructor + @PersistenceCreator public AirportJsonValue(String key, String iata, String icao) { this.key = key; this.iata = iata; diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java index 76b5ff8f7..a1d1a9a58 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java index bd7f29ac6..ebb1b34e4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java b/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java index bca35746c..ad6555921 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportMini.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -19,7 +19,7 @@ import java.util.Objects; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.couchbase.core.mapping.Document; /** @@ -34,11 +34,11 @@ public class AirportMini extends ComparableEntity { private String iata; private Address address; - @PersistenceConstructor - public AirportMini(final String id, final String iata) { - this.id = id; - this.iata = iata; - } + @PersistenceCreator + public AirportMini(final String id, final String iata) { + this.id = id; + this.iata = iata; + } public String getId() { return id; diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 1256016a9..5fb4b9553 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -40,6 +40,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -154,6 +155,11 @@ Long countFancyExpression(@Param("projectIds") List projectIds, @Param(" @Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND iata != $1") Page getAllByIataNot(String iata, Pageable pageable); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + @Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND iata != $1") + List getAllByIataNotSort(String iata, Sort sort); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) @Query("SELECT iata, \"\" as __id, 0 as __cas from #{#n1ql.bucket} WHERE #{#n1ql.filter} order by meta().id") List getStrings(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryAnnotated.java index 2fc064ad0..50c7819a7 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryAnnotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryScanConsistencyTest.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryScanConsistencyTest.java index 767e5bd46..89bf638ad 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryScanConsistencyTest.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepositoryScanConsistencyTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/AssessmentDO.java b/src/test/java/org/springframework/data/couchbase/domain/AssessmentDO.java index 0146709af..3b4728f25 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AssessmentDO.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AssessmentDO.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,9 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.Data; -import lombok.NoArgsConstructor; - import org.springframework.data.annotation.Id; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; @@ -30,8 +27,6 @@ * @author Michael Reiche */ @Document() -@Data -@NoArgsConstructor public class AssessmentDO { @Id @GeneratedValue(strategy = GenerationStrategy.USE_ATTRIBUTES) private String documentId; @@ -40,4 +35,38 @@ public class AssessmentDO { @Field("docType") private String documentType; @Field private String id; + + public String getId() { + return id; + } + + public String getDocumentId() { + return documentId; + } + + public void setEventTimestamp(long eventTimestamp) { + this.eventTimestamp = eventTimestamp; + } + + public void setId(String id) { + this.id = id; + } + + public boolean equals(Object other) { + if (other == null || !(other instanceof AssessmentDO)) { + return false; + } + AssessmentDO that = (AssessmentDO) other; + return equals(this.id, that.id) && equals(this.documentId, that.documentId) + && equals(this.eventTimestamp, that.eventTimestamp) && equals(this.documentType, that.documentType); + } + + boolean equals(Object s0, Object s1) { + if (s0 == null && s1 == null || s0 == s1) { + return true; + } + Object sa = s0 != null ? s0 : s1; + Object sb = s0 != null ? s1 : s0; + return sa.equals(sb); + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/BigAirline.java b/src/test/java/org/springframework/data/couchbase/domain/BigAirline.java new file mode 100644 index 000000000..f54d640e0 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/BigAirline.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2025 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.couchbase.domain; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.couchbase.core.mapping.Document; + +@Document +/** + * @author Michael Reiche + */ +public class BigAirline extends Airline { + BigInteger airlineNumber = new BigInteger("88881234567890123456"); // less than 63 bits, otherwise query truncates + BigDecimal airlineDecimal = new BigDecimal("888812345678901.23"); // less than 53 bits in mantissa + + @PersistenceCreator + public BigAirline(String id, String name, String hqCountry, Number airlineNumber, Number airlineDecimal) { + super(id, name, hqCountry); + this.airlineNumber = airlineNumber != null && !airlineNumber.equals("") + ? new BigInteger(airlineNumber.toString()) + : this.airlineNumber; + this.airlineDecimal = airlineDecimal != null && !airlineDecimal.equals("") + ? new BigDecimal(airlineDecimal.toString()) + : this.airlineDecimal; + } + + public BigInteger getAirlineNumber() { + return airlineNumber; + } + + public BigDecimal getAirlineDecimal() { + return airlineDecimal; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/BigAirlineRepository.java b/src/test/java/org/springframework/data/couchbase/domain/BigAirlineRepository.java new file mode 100644 index 000000000..8534ee2d6 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/BigAirlineRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2025 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.couchbase.domain; + +import java.util.List; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Michael Reiche + */ +@Repository +public interface BigAirlineRepository extends CouchbaseRepository, + DynamicProxyable { + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)") + List getByName(@Param("airline_name") String airlineName); + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/CapellaConnectSample.java b/src/test/java/org/springframework/data/couchbase/domain/CapellaConnectSample.java index cb3b038f8..ab018e3cf 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/CapellaConnectSample.java +++ b/src/test/java/org/springframework/data/couchbase/domain/CapellaConnectSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java b/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java index e133291f0..af5b5e992 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index c57a96ea3..047766660 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -211,8 +211,7 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte // that has an getAliasFor(info) that just returns getType().getName(). // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will // use the DocumentType annotation - MappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext, typeKey()); - converter.setCustomConversions(couchbaseCustomConversions); + MappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext, typeKey(), couchbaseCustomConversions); return converter; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java b/src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java similarity index 74% rename from src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java rename to src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java index 0d2995cb4..e67a69239 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -17,13 +17,11 @@ package org.springframework.data.couchbase.domain; /** - * Config to be used for testing scopes and collections. - * * @author Michael Reiche */ -public class CollectionsConfig extends Config { - @Override - public String getScopeName() { - return "my_scope"; - } +public class ConfigScoped extends Config{ + @Override + protected java.lang.String getScopeName() { + return "my_scope"; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Course.java b/src/test/java/org/springframework/data/couchbase/domain/Course.java index 46701efff..58715ee55 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Course.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Course.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java b/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java index b8cfd6797..f7c0d60c8 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java +++ b/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.domain; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -37,4 +38,21 @@ public CustomMappingCouchbaseConverter( this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey); } + /** + * this constructer creates a TypeBasedCouchbaseTypeMapper with the specified couchbaseCustomConversions and typeKey + * while MappingCouchbaseConverter uses a DefaultCouchbaseTypeMapper typeMapper = new DefaultCouchbaseTypeMapper(typeKey != null ? typeKey : + * TYPEKEY_DEFAULT); + * + * @param mappingContext + * @param typeKey - the typeKey to be used (normally "_class") + * @param couchbaseCustomConversions - custom conversions to use + */ + public CustomMappingCouchbaseConverter( + final MappingContext, CouchbasePersistentProperty> mappingContext, + final String typeKey, + final CouchbaseCustomConversions couchbaseCustomConversions) { + super(mappingContext, typeKey, couchbaseCustomConversions); + this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java index 94290ce64..3fe41a9f4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java +++ b/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java @@ -1,7 +1,7 @@ package org.springframework.data.couchbase.domain; /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java index 3bbe57a5a..01b61710e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java +++ b/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java index 9d01c47ce..9dc7dd003 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java +++ b/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java index 58a71bc47..581388bf6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java index ab29839df..f193780df 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ParallelFlux; @@ -33,14 +34,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -64,7 +63,8 @@ /** * @author Michael Reiche */ -@SpringJUnitConfig(FluxIntegrationTests.Config.class) +@SpringJUnitConfig(Config.class) +@DirtiesContext @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class FluxIntegrationTests extends JavaIntegrationTests { @@ -74,21 +74,17 @@ public class FluxIntegrationTests extends JavaIntegrationTests { @BeforeEach @Override public void beforeEach() { - + super.beforeEach(); /** * The couchbaseTemplate inherited from JavaIntegrationTests uses org.springframework.data.couchbase.domain.Config * It has typeName = 't' (instead of _class). Don't use it. */ - ApplicationContext ac = new AnnotationConfigApplicationContext(FluxIntegrationTests.Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(BeanNames.COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(BeanNames.REACTIVE_COUCHBASE_TEMPLATE); collection = couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection(); rCollection = couchbaseTemplate.getCouchbaseClientFactory().getBucket().reactive().defaultCollection(); for (String k : keyList) { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().upsert(k, JsonObject.create().put("x", k)); } - super.beforeEach(); } @AfterEach @@ -242,37 +238,4 @@ static String tab(int len) { return sb.toString(); } - @Configuration - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Library.java b/src/test/java/org/springframework/data/couchbase/domain/Library.java index 06fb4ebda..65d8ba99b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Library.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Library.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/LibraryRepository.java b/src/test/java/org/springframework/data/couchbase/domain/LibraryRepository.java index 78ccdac3f..16afa96d1 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/LibraryRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/LibraryRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java b/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java index ffaee64f7..3cb2bf04a 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java +++ b/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java @@ -1,28 +1,42 @@ package org.springframework.data.couchbase.domain; -import lombok.Getter; -import lombok.Setter; -import org.springframework.data.couchbase.core.mapping.Document; - import java.util.List; +import org.springframework.data.couchbase.core.mapping.Document; + @Document public class MutableUser extends User{ public MutableUser(String id, String firstname, String lastname) { super(id, firstname, lastname); } - - @Getter - @Setter + private Address address; - @Getter - @Setter private MutableUser subuser; - @Getter - @Setter private List roles; - - + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Address getAddress() { + return address; + } + + public void setSubuser(MutableUser subuser) { + this.subuser = subuser; + } + + public MutableUser getSubuser() { + return subuser; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/MyPerson.java b/src/test/java/org/springframework/data/couchbase/domain/MyPerson.java new file mode 100644 index 000000000..460b79bb6 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/MyPerson.java @@ -0,0 +1,25 @@ +package org.springframework.data.couchbase.domain; + +import jakarta.validation.constraints.NotNull; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Field; + +public class MyPerson { + @NotNull + @Id + public String id; + + @Field + public Object myObject; + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("MyPerson:{"); + sb.append("id:"); + sb.append(id); + sb.append(", myObject:"); + sb.append(myObject); + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/MyPersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/MyPersonRepository.java new file mode 100644 index 000000000..973c9144d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/MyPersonRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2025 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.couchbase.domain; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.repository.query.Param; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * @author Michael Reiche + */ +public interface MyPersonRepository extends CouchbaseRepository, DynamicProxyable { + +} + diff --git a/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java index b04e5fccb..dbaf94839 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java +++ b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java index 4a9c102bb..1a1f8025c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java +++ b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.domain; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.couchbase.core.mapping.Document; @@ -30,7 +30,7 @@ @TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER) public class OtherUser extends AbstractUser { - @PersistenceConstructor + @PersistenceCreator public OtherUser(final String id, final String firstname, final String lastname) { this.id = id; this.firstname = firstname; diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index cd0f83019..d30dfea39 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; import org.springframework.data.domain.Persistable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Person entity for tests. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index 15db55a30..7f76f490f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonValue.java b/src/test/java/org/springframework/data/couchbase/domain/PersonValue.java index 10f68a803..a89aac043 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonValue.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -15,8 +15,6 @@ */ package org.springframework.data.couchbase.domain; -import lombok.Value; -import lombok.With; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; @@ -30,16 +28,13 @@ * @author Michael Reiche */ -@Value @Document public class PersonValue { @Id @GeneratedValue(strategy = GenerationStrategy.UNIQUE) - @With String id; - @Version - @With - long version; - @Field String firstname; - @Field String lastname; + private final String id; + @Version private final long version; + @Field private final String firstname; + @Field private final String lastname; public PersonValue(String id, long version, String firstname, String lastname) { this.id = id; @@ -48,10 +43,26 @@ public PersonValue(String id, long version, String firstname, String lastname) { this.lastname = lastname; } + public PersonValue withId(String id) { + return new PersonValue(id, this.version, this.firstname, this.lastname); + } + + public PersonValue withVersion(Long version) { + return new PersonValue(this.id, version, this.firstname, this.lastname); + } + + public PersonValue withFirstname(String firstname) { + return new PersonValue(this.id, this.version, firstname, this.lastname); + } + + public PersonValue withLastname(String lastname) { + return new PersonValue(this.id, this.version, this.firstname, lastname); + } + public String toString() { StringBuilder sb = new StringBuilder(); sb.append("PersonValue : {"); - sb.append(" id : " + getId()); + sb.append(" id : " + id); sb.append(", version : " + version); sb.append(", firstname : " + firstname); sb.append(", lastname : " + lastname); @@ -59,4 +70,29 @@ public String toString() { return sb.toString(); } + public String getId() { + return id; + } + + public long getVersion() { + return version; + } + + public boolean equals(Object other) { + if (other == null || !(other instanceof PersonValue)) { + return false; + } + PersonValue that = (PersonValue) other; + return equals(this.getId(), that.getId()) && equals(this.version, that.version) + && equals(this.firstname, that.firstname) && equals(this.lastname, that.lastname); + } + + boolean equals(Object s0, Object s1) { + if (s0 == null && s1 == null || s0 == s1) { + return true; + } + Object sa = s0 != null ? s0 : s1; + Object sb = s0 != null ? s1 : s0; + return sa.equals(sb); + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonValueRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonValueRepository.java index 6d1ed533e..88f6aafc0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonValueRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonValueRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java index a770dee26..c49c53939 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java index a0877486f..4f991b616 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithMaps.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithMaps.java new file mode 100644 index 000000000..285ccf2db --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithMaps.java @@ -0,0 +1,62 @@ +package org.springframework.data.couchbase.domain; + +import java.util.Map; +import java.util.Set; + +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.Field; + +import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Document +public class PersonWithMaps { + + @Id + private String id; + @Field + private Map> versions; + @Field + private Map> releaseVersions; + + public PersonWithMaps(){ + } + + public void setId(String id) { + this.id = id; + } + + public void setVersions(Map> versions) { + this.versions = versions; + } + + public void setReleaseVersions(Map> releaseVersions) { + this.releaseVersions = releaseVersions; + } + + public String getId() { + return id; + } + + public boolean equals(Object other) { + if (other == null || !(other instanceof PersonWithMaps)) { + return false; + } + PersonWithMaps that = (PersonWithMaps) other; + return equals(this.getId(), that.getId()) && equals(this.versions, that.versions) + && equals(this.releaseVersions, that.releaseVersions); + } + + boolean equals(Object s0, Object s1) { + if (s0 == null && s1 == null || s0 == s1) { + return true; + } + Object sa = s0 != null ? s0 : s1; + Object sb = s0 != null ? s1 : s0; + return sa.equals(sb); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java index 13dfe8d32..ed3494804 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -46,4 +46,25 @@ public PersonWithoutVersion(UUID id, String firstname, String lastname) { this.lastname = Optional.of(lastname); setId(id); } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PersonWithoutVersion : {\n"); + sb.append(" id : " + getId()); + sb.append(optional(", firstname", firstname)); + sb.append(optional(", lastname", lastname)); + sb.append("\n}"); + return sb.toString(); + } + + static String optional(String name, Optional obj) { + if (obj != null) { + if (obj.isPresent()) { + return (" " + name + ": '" + obj.get() + "'"); + } else { + return " " + name + ": null"; + } + } + return ""; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirlineRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirlineRepository.java index 758b41290..b7312f716 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirlineRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirlineRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportMustScopeRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportMustScopeRepository.java new file mode 100644 index 000000000..225c4d171 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportMustScopeRepository.java @@ -0,0 +1,9 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.Scope; + +@Scope("must set scope name") +@Collection("my_collection") +public interface ReactiveAirportMustScopeRepository extends ReactiveAirportRepository { +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 858f199ba..42870920e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -54,6 +54,9 @@ public interface ReactiveAirportRepository @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Flux findAll(); + @Query("#{#n1ql .selectEntity} WHERE #{#n1ql.filter} ORDER BY $3 $4 LIMIT $1 OFFSET $2 ") + Flux findAllTestPrimitives(int iint, long llong, double ddouble, boolean bbolean); + @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Mono deleteAll(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepositoryAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepositoryAnnotated.java index 19c1e6d78..405ff3c35 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepositoryAnnotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepositoryAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java index 1ea140f12..c13ebd5b9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java index 23f531cf1..0f0205b57 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java index 76c7d7dd1..2052fcdc2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserRepository.java index 1d0a60f01..e0213153a 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/Submission.java b/src/test/java/org/springframework/data/couchbase/domain/Submission.java index 964790205..acf93aa31 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Submission.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Submission.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/SubscriptionToken.java b/src/test/java/org/springframework/data/couchbase/domain/SubscriptionToken.java index 85dc710e2..e6bb6d7b9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/SubscriptionToken.java +++ b/src/test/java/org/springframework/data/couchbase/domain/SubscriptionToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,9 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; @@ -31,9 +28,6 @@ * * @author Michael Reiche */ -@Getter -@ToString -@EqualsAndHashCode @Document public class SubscriptionToken { private @Id @@ -72,4 +66,12 @@ public SubscriptionToken( public void setType(String type) { type = type; } + + public long getVersion() { + return version; + } + + public String getId() { + return id; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/SubscriptionTokenRepository.java b/src/test/java/org/springframework/data/couchbase/domain/SubscriptionTokenRepository.java index 403894772..b9da11482 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/SubscriptionTokenRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/SubscriptionTokenRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/TestEncrypted.java b/src/test/java/org/springframework/data/couchbase/domain/TestEncrypted.java index 362c6c97f..2b6c45a65 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/TestEncrypted.java +++ b/src/test/java/org/springframework/data/couchbase/domain/TestEncrypted.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -17,25 +17,11 @@ package org.springframework.data.couchbase.domain; import java.io.Serializable; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; import java.util.Objects; -import java.util.UUID; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.data.annotation.TypeAlias; -import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import com.couchbase.client.java.encryption.annotation.Encrypted; -import com.couchbase.client.java.query.QueryScanConsistency; /** * UserEncrypted entity for tests diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index b87bd0515..853e51a5f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -17,18 +17,26 @@ package org.springframework.data.couchbase.domain; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * User entity for tests * @@ -40,14 +48,34 @@ @TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER) public class User extends AbstractUser implements Serializable { - @PersistenceConstructor + public JsonNode jsonNode; + public JsonObject jsonObject; + public JsonArray jsonArray; + + @PersistenceCreator public User(final String id, final String firstname, final String lastname) { this.id = id; this.firstname = firstname; this.lastname = lastname; this.subtype = AbstractingTypeMapper.Type.USER; + this.jsonNode = new ObjectNode(JsonNodeFactory.instance); + try { + jsonNode = (new ObjectNode(JsonNodeFactory.instance)).put("myNumber", uid()); + } catch (Exception e) { + e.printStackTrace(); + } + Map map = new HashMap(); + map.put("myNumber", uid()); + this.jsonObject = JsonObject.jo().put("yourNumber",Long.valueOf(uid())); + this.jsonArray = JsonArray.from(Long.valueOf(uid()), Long.valueOf(uid())); } + @Transient int uid=1000; + long uid(){ + return uid++; + } + + @Version protected long version; @Transient protected String transientInfo; @CreatedBy protected String createdBy; diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated.java index 266b0509d..316380a6e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,15 +16,6 @@ package org.springframework.data.couchbase.domain; -import java.util.Objects; - -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; /** diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated2.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated2.java index 47ad9edbd..526b1e6e7 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated2.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated2.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated3.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated3.java index 9540bb944..2953a7104 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated3.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotated3.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java index ccdd6df75..e359011db 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurabilityExpression.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurabilityExpression.java new file mode 100644 index 000000000..e78431a7c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurabilityExpression.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2025 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.couchbase.domain; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.io.Serializable; + +/** + * Annotated User entity for tests + * + * @author Tigran Babloyan + */ + +@Document(durabilityExpression = "${valid.document.durability}") +public class UserAnnotatedDurabilityExpression extends User implements Serializable { + + public UserAnnotatedDurabilityExpression(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java index 96fd16b7f..9b6e303f3 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java index db6fee247..e3ffff6cc 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java index 2b80ad8bd..5c66f7903 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserCol.java b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java index 872b222f7..3ed2b7e16 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserCol.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.domain; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.Scope; @@ -33,7 +33,7 @@ @Collection("other_collection") public class UserCol extends User { - @PersistenceConstructor + @PersistenceCreator public UserCol(final String id, final String firstname, final String lastname) { super(id, firstname, lastname); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java index 247054f85..4592e439e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserEncrypted.java b/src/test/java/org/springframework/data/couchbase/domain/UserEncrypted.java index 24137faed..fd45e45b2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserEncrypted.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserEncrypted.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -29,7 +29,7 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; @@ -55,7 +55,7 @@ public UserEncrypted() { public String _class; // cheat a little so that will work with Java SDK - @PersistenceConstructor + @PersistenceCreator public UserEncrypted(final String id, final String firstname, final String lastname) { this(); this.id = id; diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserEncryptedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserEncryptedRepository.java index 27f96f30e..19f5978a0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserEncryptedRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserEncryptedRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java b/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java index 557ce67c1..dffc69269 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -19,7 +19,7 @@ import java.util.Objects; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.couchbase.core.mapping.Document; /** @@ -37,7 +37,7 @@ public class UserJustLastName extends ComparableEntity { public User user; - @PersistenceConstructor + @PersistenceCreator public UserJustLastName(final String id, final String lastname) { this.id = id; this.lastname = lastname; diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserNoAlias.java b/src/test/java/org/springframework/data/couchbase/domain/UserNoAlias.java new file mode 100644 index 000000000..d868dc8b5 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserNoAlias.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2025 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.couchbase.domain; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.annotation.Transient; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.core.mapping.Document; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * User entity with an empty TypeAlias for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ + +@Document +@TypeAlias("") +public class UserNoAlias extends AbstractUser implements Serializable { + + public JsonNode jsonNode; + public JsonObject jsonObject; + public JsonArray jsonArray; + + @PersistenceCreator + public UserNoAlias(final String id, final String firstname, final String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.subtype = AbstractingTypeMapper.Type.USER; + this.jsonNode = new ObjectNode(JsonNodeFactory.instance); + try { + jsonNode = (new ObjectNode(JsonNodeFactory.instance)).put("myNumber", uid()); + } catch (Exception e) { + e.printStackTrace(); + } + Map map = new HashMap(); + map.put("myNumber", uid()); + this.jsonObject = JsonObject.jo().put("yourNumber",Long.valueOf(uid())); + this.jsonArray = JsonArray.from(Long.valueOf(uid()), Long.valueOf(uid())); + } + + @Transient int uid=1000; + long uid(){ + return uid++; + } + + + @Version protected long version; + @Transient protected String transientInfo; + @CreatedBy protected String createdBy; + @CreatedDate protected long createdDate; + @LastModifiedBy protected String lastModifiedBy; + @LastModifiedDate protected long lastModifiedDate; + + public String getLastname() { + return lastname; + } + + public long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(long createdDate) { + this.createdDate = createdDate; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public long getLastModifiedDate() { + return lastModifiedDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + @Override + public int hashCode() { + return Objects.hash(getId(), firstname, lastname); + } + + public String getTransientInfo() { + return transientInfo; + } + + public void setTransientInfo(String something) { + transientInfo = something; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 79f20da4f..637e65173 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -19,6 +19,7 @@ import java.util.List; import java.util.stream.Stream; +import com.fasterxml.jackson.annotation.JsonValue; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.CouchbaseRepository; @@ -61,6 +62,41 @@ public interface UserRepository extends CouchbaseRepository { List findByIdIsNotNullAndFirstnameEquals(String firstname); + List findByFirstname(@Param("firstName")FirstName firstName ); + + List findByFirstnameIn(@Param("firstNames")FirstName[] firstNames ); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $firstName)") + List queryByFirstnameNamedParameter(@Param("firstName")FirstName firstName ); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $1)") + List queryByFirstnamePositionalParameter(@Param("firstName")FirstName firstName ); + + enum FirstName { + Dave, + William + } + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (jsonNode.myNumber = $myNumber)") + List queryByIntegerEnumNamed(@Param("myNumber")IntEnum myNumber ); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (jsonNode.myNumber = $1)") + List queryByIntegerEnumPositional(@Param("myNumber")IntEnum myNumber ); + + enum IntEnum { + One(1), + Two(2), + OneThousand(1000); + Integer value; + IntEnum(Integer i){ + value = i; + } + @JsonValue + public Integer getValue(){ + return value; + } + } + List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); @Query("#{#n1ql.selectEntity}|#{#n1ql.filter}|#{#n1ql.bucket}|#{#n1ql.scope}|#{#n1ql.collection}") diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java index 7f05b2cba..877d4cbea 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.Data; - import java.util.List; import org.springframework.data.annotation.TypeAlias; @@ -31,7 +29,6 @@ * * @author Michael Reiche */ -@Data @Document @TypeAlias("user") @CompositeQueryIndex(fields = { "id", "username", "email" }) @@ -56,4 +53,43 @@ public void setCourses(List courses) { this.courses = courses; } + public void setId(String id) { + this.id = id; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getId() { + return id; + } + + public Address getAddress() { + return address; + } + + public List getCourses() { + return courses; + } + + public String getUsername() { + return username; + } + + public List
getOtherAddresses() { + return otherAddresses; + } + + public List getSubmissions() { + return submissions; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public void setAddress(Address address) { + this.address = address; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java index 3ca25accf..85eea52d2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,23 +16,20 @@ package org.springframework.data.couchbase.domain; -import lombok.Data; +import java.util.List; + import org.springframework.data.annotation.TypeAlias; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.core.mapping.Field; import org.springframework.data.couchbase.core.query.FetchType; import org.springframework.data.couchbase.core.query.N1qlJoin; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.Scope; -import java.util.List; - /** * UserSubmissionAnnotated entity for tests * * @author Michael Reiche */ -@Data @Document @TypeAlias("user") @Scope("my_scope") @@ -57,4 +54,23 @@ public void setCourses(List courses) { this.courses = courses; } + public void setId(String id) { + this.id = id; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getId() { + return id; + } + + public List getOtherAddresses() { + return otherAddresses; + } + + public String getUsername() { + return username; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java index 7f72fa523..9680902d5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java index db4d17f7c..e5fad8db6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.Data; - import java.util.List; import org.springframework.data.annotation.TypeAlias; @@ -29,7 +27,6 @@ * * @author Michael Reiche */ -@Data @Document @TypeAlias("user") @CompositeQueryIndex(fields = { "id", "username", "email" }) @@ -44,4 +41,19 @@ public void setCourses(List courses) { this.courses = courses; } + public String getUsername() { + return username; + } + + public String getId() { + return id; + } + + public List getCourses() { + return courses; + } + + public Address getAddress() { + return address; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java index 3af000e58..9134b2e3e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java index a6bdf8609..84a610d9b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.Data; - import java.util.List; import org.springframework.data.annotation.TypeAlias; @@ -31,7 +29,6 @@ * * @author Michael Reiche */ -@Data @Document // there is no @Scope annotation on this entity @Collection("my_collection") @@ -56,4 +53,23 @@ public void setCourses(List courses) { this.courses = courses; } + public void setId(String id) { + this.id = id; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public List getOtherAddresses() { + return otherAddresses; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java index 049e4f14d..a89c3f27a 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java b/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java index cfc83ca9a..5c16324ef 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java +++ b/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java index 80873fe2c..fc36f4f1a 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java +++ b/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java index 802af15a6..e1f7e9440 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java +++ b/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java index 927bf4676..0e56752c6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java +++ b/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java index 254af4c63..04c9276c1 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -39,6 +39,7 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -51,6 +52,7 @@ * @author Michael Reiche */ @SpringJUnitConfig(CouchbaseAbstractRepositoryIntegrationTests.Config.class) +@DirtiesContext @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class CouchbaseAbstractRepositoryIntegrationTests extends ClusterAwareIntegrationTests { @@ -108,38 +110,7 @@ void saveAndFindAbstract() { } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - + static class Config extends org.springframework.data.couchbase.domain.Config { /** * This uses a CustomMappingCouchbaseConverter instead of MappingCouchbaseConverter, which in turn uses * AbstractTypeMapper which has special mapping for AbstractUser @@ -153,8 +124,8 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will // use the DocumentType annotation MappingCouchbaseConverter converter = new AbstractingMappingCouchbaseConverter(couchbaseMappingContext, - typeKey()); - converter.setCustomConversions(couchbaseCustomConversions); + typeKey(), + couchbaseCustomConversions); return converter; } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java index 951e5d705..4df454de6 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -32,12 +32,14 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.manager.query.QueryIndex; @SpringJUnitConfig(CouchbaseRepositoryAutoQueryIndexIntegrationTests.Config.class) +@DirtiesContext @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryAutoQueryIndexIntegrationTests extends ClusterAwareIntegrationTests { diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.java index 7730747f3..99774cfa5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -42,10 +42,10 @@ import org.springframework.data.couchbase.domain.TestEncrypted; import org.springframework.data.couchbase.domain.UserEncrypted; import org.springframework.data.couchbase.domain.UserEncryptedRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -66,6 +66,7 @@ * @author Michael Reiche */ @SpringJUnitConfig(CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.Config.class) +@DirtiesContext @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryFieldLevelEncryptionIntegrationTests extends ClusterAwareIntegrationTests { @@ -153,10 +154,14 @@ void writeSpring_readSpring() { assertFalse(userEncryptedRepository.existsById(user.getId())); userEncryptedRepository.save(user); // read the user with Spring + Optional writeSpringReadSpring = userEncryptedRepository.findById(user.getId()); assertTrue(writeSpringReadSpring.isPresent()); writeSpringReadSpring.ifPresent(u -> assertEquals(user, u)); + List writeSpringReadSpring2 = userEncryptedRepository.findAll(); + assertEquals(user, writeSpringReadSpring2.stream().filter(u -> u.getId().equals(user.getId())).findFirst().get()); + if (cleanAfter) { try { couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId()); @@ -329,29 +334,7 @@ void testFromMigration() { } } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } + static class Config extends org.springframework.data.couchbase.domain.Config { @Override public ObjectMapper couchbaseObjectMapper() { @@ -360,14 +343,6 @@ public ObjectMapper couchbaseObjectMapper() { return om; } - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - @Override protected CryptoManager cryptoManager() { Map keyMap = new HashMap(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 4c31557be..9fc18f579 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -33,13 +33,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.AirlineRepository; +import org.springframework.data.couchbase.domain.BigAirline; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.Library; import org.springframework.data.couchbase.domain.LibraryRepository; @@ -52,15 +52,12 @@ import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.domain.UserSubmissionRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.kv.GetResult; /** @@ -69,7 +66,8 @@ * @author Michael Nitschinger * @author Michael Reiche */ -@SpringJUnitConfig(CouchbaseRepositoryKeyValueIntegrationTests.Config.class) +@SpringJUnitConfig(Config.class) +@DirtiesContext @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareIntegrationTests { @@ -113,7 +111,7 @@ void saveReplaceUpsertInsert() { assertThrows(DuplicateKeyException.class, () -> userRepository.save(user)); user.setVersion(saveVersion + 1); assertThrows(OptimisticLockingFailureException.class, () -> userRepository.save(user)); - userRepository.delete(user); + userRepository.deleteById(user.getId()); // Airline does not have a version Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null); @@ -123,6 +121,17 @@ void saveReplaceUpsertInsert() { airlineRepository.delete(airline); } + @Test + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) + void saveBig() { + BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null); + airline = airlineRepository.save(airline); + Optional foundMaybe = airlineRepository.findById(airline.getId()); + BigAirline found = (BigAirline) foundMaybe.get(); + assertEquals(found, airline); + airlineRepository.delete(airline); + } + @Test @IgnoreWhen(clusterTypes = ClusterType.MOCKED) void saveAndFindById() { @@ -198,37 +207,4 @@ void saveAndFindByWithNestedId() { userSubmissionRepository.delete(user); } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - } - } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index cdc7cd35e..7e0f4d75d 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -37,6 +37,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -57,9 +58,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; @@ -67,6 +66,7 @@ import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.AirlineRepository; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportJsonValue; @@ -80,16 +80,20 @@ import org.springframework.data.couchbase.domain.EJsonCreatorTurbulenceCategory; import org.springframework.data.couchbase.domain.ETurbulenceCategory; import org.springframework.data.couchbase.domain.Iata; +import org.springframework.data.couchbase.domain.MyPerson; +import org.springframework.data.couchbase.domain.MyPersonRepository; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactiveAirportRepository; +import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserAnnotated; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.domain.UserSubmissionRepository; -import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.auditing.EnableReactiveCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; import org.springframework.data.couchbase.repository.query.CouchbaseRepositoryQuery; @@ -104,20 +108,19 @@ import org.springframework.data.domain.Sort; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.error.IndexFailureException; -import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.core.error.UnambiguousTimeoutException; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -130,12 +133,15 @@ */ @SpringJUnitConfig(CouchbaseRepositoryQueryIntegrationTests.Config.class) @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@DirtiesContext public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegrationTests { @Autowired CouchbaseClientFactory clientFactory; @Autowired AirportRepository airportRepository; + @Autowired ReactiveAirportRepository reactiveAirportRepository; + @Autowired AirportJsonValueRepository airportJsonValueRepository; @Autowired AirlineRepository airlineRepository; @@ -144,6 +150,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired UserSubmissionRepository userSubmissionRepository; + @Autowired MyPersonRepository myPersonRepository; + + @Autowired CouchbaseTemplate couchbaseTemplate; String scopeName = "_default"; @@ -175,6 +184,22 @@ void shouldSaveAndFindAll() { } } + @Test + void findMyPerson() { + MyPerson vie = null; + try { + vie = new MyPerson(); + vie.id = "123"; UUID.randomUUID().toString(); + vie.myObject = Collections.singletonList("a"); + MyPerson p = myPersonRepository.save(vie); + System.err.println(p); + Optional r = myPersonRepository.findById( p.id); + System.err.println(r.get()); + } finally { + try { myPersonRepository.delete(vie); } catch (DataRetrievalFailureException dnfe){} + } + } + @Test void shouldNotSave() { Airport vie = new Airport("airports::vie", "vie", "low4"); @@ -290,6 +315,23 @@ void issue1304CollectionParameter() { } + @Test + void issuePageableDynamicProxyParameter() { + Airline airline = null; + try { + airline = new Airline("airline::USA", "US Air", "US"); + airlineRepository.withScope("_default").save(airline); + java.util.Collection countries = new LinkedList(); + countries.add(airline.getHqCountry()); + Pageable pageable = PageRequest.of(0, 1, Sort.by("hqCountry")); + Page airports2 = airlineRepository.withScope("_default").withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).findByHqCountryIn(countries, pageable); + assertEquals(1, airports2.getTotalElements()); + } finally { + airlineRepository.withScope("_default").delete(airline); + } + + } + @Test void findBySimpleProperty() { Airport vie = null; @@ -447,85 +489,100 @@ public void saveNotBoundedRequestPlus() { CouchbaseTemplate couchbaseTemplateRP = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); AirportRepository airportRepositoryRP = (AirportRepository) ac.getBean("airportRepository"); - // save() followed by query with NOT_BOUNDED will result in not finding the document - Airport vie = new Airport("airports::vie", "vie", "low9"); - Airport airport2 = null; - for (int i = 1; i <= 100; i++) { - // set version == 0 so save() will be an upsert, not a replace - Airport saved = airportRepositoryRP.save(vie.clearVersion()); - try { - airport2 = airportRepositoryRP.iata(saved.getIata()); - if (airport2 == null) { - break; - } - } catch (DataRetrievalFailureException drfe) { - airport2 = null; // - } finally { - // airportRepository.delete(vie); - // instead of delete, use removeResult to test QueryOptions.consistentWith() - RemoveResult removeResult = couchbaseTemplateRP.removeById().one(vie.getId()); - assertEquals(vie.getId(), removeResult.getId()); - assertTrue(removeResult.getCas() != 0); - assertTrue(removeResult.getMutationToken().isPresent()); - Airport airport3 = airportRepositoryRP.iata(vie.getIata()); - assertNull(airport3, "should have been removed"); - } - } - assertNotNull(airport2, "airport2 should have never been null"); - Airport saved = airportRepositoryRP.save(vie.clearVersion()); - List airports = couchbaseTemplateRP.findByQuery(Airport.class).withConsistency(NOT_BOUNDED).all(); - RemoveResult removeResult = couchbaseTemplateRP.removeById().one(saved.getId()); - if (!config().isUsingCloud()) { - assertTrue(airports.isEmpty(), "airports should have been empty"); + try { + // save() followed by query with NOT_BOUNDED will result in not finding the document + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport airport2 = null; + for (int i = 1; i <= 100; i++) { + // set version == 0 so save() will be an upsert, not a replace + Airport saved = couchbaseTemplateRP.upsertById(Airport.class) + .withOptions(UpsertOptions.upsertOptions().timeout(Duration.ofSeconds(10))) + .one(vie.clearVersion()); + try { + airport2 = airportRepositoryRP.iata(saved.getIata()); + if (airport2 == null) { + break; + } + } catch (DataRetrievalFailureException drfe) { + airport2 = null; // + } finally { + // airportRepository.delete(vie); + // instead of delete, use removeResult to test QueryOptions.consistentWith() + RemoveResult removeResult = couchbaseTemplateRP.removeById().one(vie.getId()); + assertEquals(vie.getId(), removeResult.getId()); + assertTrue(removeResult.getCas() != 0); + assertTrue(removeResult.getMutationToken().isPresent()); + Airport airport3 = airportRepositoryRP.iata(vie.getIata()); + assertNull(airport3, "should have been removed"); + } + } + assertNotNull(airport2, "airport2 should have never been null"); + Airport saved = airportRepositoryRP.save(vie.clearVersion()); + List airports = couchbaseTemplateRP.findByQuery(Airport.class).withConsistency(NOT_BOUNDED).all(); + RemoveResult removeResult = couchbaseTemplateRP.removeById().one(saved.getId()); + if (!config().isUsingCloud()) { + assertTrue(airports.isEmpty(), "airports should have been empty"); + } + } finally { + logDisconnect(couchbaseTemplateRP.getCouchbaseClientFactory().getCluster(), + this.getClass().getSimpleName()); } } @Test - public void saveNotBoundedWithDefaultRepository() { - if (config().isUsingCloud()) { // I don't think the query following the insert will be quick enough for the test - return; - } - airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll(); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - // the Config class has been modified, these need to be loaded again - CouchbaseTemplate couchbaseTemplateRP = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - AirportRepositoryScanConsistencyTest airportRepositoryRP = (AirportRepositoryScanConsistencyTest) ac - .getBean("airportRepositoryScanConsistencyTest"); - - List sizeBeforeTest = (List) airportRepositoryRP.findAll(); - assertEquals(0, sizeBeforeTest.size()); - - boolean notFound = false; - for (int i = 0; i < 100; i++) { - Airport vie = new Airport("airports::vie", "vie", "low9"); - Airport saved = airportRepositoryRP.save(vie); - List allSaved = (List) airportRepositoryRP.findAll(); - couchbaseTemplate.removeById(Airport.class).one(saved.getId()); - if (allSaved.isEmpty()) { - notFound = true; - break; - } - } - assertTrue(notFound, "the doc should not have been found. maybe"); - } + public void saveNotBoundedWithDefaultRepository() { + if (config().isUsingCloud()) { // I don't think the query following the insert will be quick enough for the test + return; + } + airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll(); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // the Config class has been modified, these need to be loaded again + + AirportRepositoryScanConsistencyTest airportRepositoryRP = (AirportRepositoryScanConsistencyTest) ac + .getBean("airportRepositoryScanConsistencyTest"); + + try { + List sizeBeforeTest = (List) airportRepositoryRP.findAll(); + assertEquals(0, sizeBeforeTest.size()); + + boolean notFound = false; + for (int i = 0; i < 100; i++) { + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport saved = airportRepositoryRP.save(vie); + List allSaved = (List) airportRepositoryRP.findAll(); + couchbaseTemplate.removeById(Airport.class).one(saved.getId()); + if (allSaved.isEmpty()) { + notFound = true; + break; + } + } + assertTrue(notFound, "the doc should not have been found. maybe"); + } finally { + logDisconnect(airportRepositoryRP.getOperations().getCouchbaseClientFactory().getCluster(), "b"); + } + } @Test - public void saveRequestPlusWithDefaultRepository() { + public void saveRequestPlusWithDefaultRepository() { - ApplicationContext ac = new AnnotationConfigApplicationContext(ConfigRequestPlus.class); - // the Config class has been modified, these need to be loaded again - AirportRepositoryScanConsistencyTest airportRepositoryRP = (AirportRepositoryScanConsistencyTest) ac - .getBean("airportRepositoryScanConsistencyTest"); + ApplicationContext ac = new AnnotationConfigApplicationContext(ConfigRequestPlus.class); + // the Config class has been modified, these need to be loaded again + AirportRepositoryScanConsistencyTest airportRepositoryRP = (AirportRepositoryScanConsistencyTest) ac + .getBean("airportRepositoryScanConsistencyTest"); - List sizeBeforeTest = airportRepositoryRP.findAll(); - assertEquals(0, sizeBeforeTest.size()); + try { + List sizeBeforeTest = airportRepositoryRP.findAll(); + assertEquals(0, sizeBeforeTest.size()); - Airport vie = new Airport("airports::vie", "vie", "low9"); - Airport saved = airportRepositoryRP.save(vie); - List allSaved = airportRepositoryRP.findAll(REQUEST_PLUS); - couchbaseTemplate.removeById(Airport.class).one(saved.getId()); - assertEquals(1, allSaved.size(), "should have found 1 airport"); - } + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport saved = airportRepositoryRP.save(vie); + List allSaved = airportRepositoryRP.findAll(REQUEST_PLUS); + couchbaseTemplate.removeById(Airport.class).one(saved.getId()); + } finally { + logDisconnect(airportRepositoryRP.getOperations().getCouchbaseClientFactory().getCluster(), "c"); + } + + } @Test void findByTypeAlias() { @@ -535,9 +592,9 @@ void findByTypeAlias() { vie = airportRepository.save(vie); List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) .matching(org.springframework.data.couchbase.core.query.Query - .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) + .query(QueryCriteria.where(N1QLExpression.x("t")).is("airport"))) .all(); - assertFalse(airports.isEmpty(), "should have found aiport"); + assertFalse(airports.isEmpty(), "should have found airport"); } finally { airportRepository.delete(vie); } @@ -644,7 +701,7 @@ void findBySimplePropertyWithOptions() { try { Airport saved = airportRepository.save(vie); // Duration of 1 nano-second will cause timeout - assertThrows(AmbiguousTimeoutException.class, + assertThrows(UnambiguousTimeoutException.class, () -> airportRepository.withOptions(queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); Airport airport3 = airportRepository @@ -704,6 +761,41 @@ public void testTransient() { userRepository.delete(user); } + @Test + public void testEnumParameter() { + User user = new User("1", "Dave", "Wilson"); + userRepository.save(user); + User user2 = new User("2", "Frank", "Spalding"); + userRepository.save(user2); + + List foundUsersEquals = userRepository.findByFirstname(UserRepository.FirstName.Dave); + assertEquals(user.getId(), foundUsersEquals.get(0).getId()); + assertEquals(1, foundUsersEquals.size()); + + List foundUsersIn = userRepository.findByFirstnameIn( new UserRepository.FirstName[]{ UserRepository.FirstName.Dave }); + assertEquals(user.getId(), foundUsersIn.get(0).getId()); + assertEquals(1, foundUsersIn.size()); + + List namedUsers = userRepository.queryByFirstnameNamedParameter( UserRepository.FirstName.Dave); + assertEquals(user.getId(), namedUsers.get(0).getId()); + assertEquals(1, namedUsers.size()); + + List positionalUsers = userRepository.queryByFirstnamePositionalParameter( UserRepository.FirstName.Dave); + assertEquals(user.getId(), positionalUsers.get(0).getId()); + assertEquals(1, positionalUsers.size()); + + // User objects are initialized with jsonNode.myNumber = 1000 + List integerEnumUsersNamed = userRepository.queryByIntegerEnumNamed(UserRepository.IntEnum.OneThousand); + assertEquals(2, integerEnumUsersNamed.size()); + + // User objects are initialized with jsonNode.myNumber = 1000 + List integerEnumUsersPositional = userRepository.queryByIntegerEnumPositional(UserRepository.IntEnum.OneThousand); + assertEquals(2, integerEnumUsersPositional.size()); + + userRepository.delete(user); + userRepository.delete(user2); + } + @Test public void testCas() { User user = new User("1", "Dave", "Wilson"); @@ -792,6 +884,31 @@ void sortedRepository() { } } + @Test + void countSlicePageTest() { + airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll(); + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + + airportRepository.countOne(); + try { + airportRepository.saveAll( + Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet())); + + Pageable sPageable = PageRequest.of(0, 2).withSort(Sort.by("iata")); + Page sPage = airportRepository.getAllByIataNot("JFK", sPageable); + System.out.println(sPage); + + Sort sort = Sort.by("iata"); + List sList = airportRepository.getAllByIataNotSort("JFK", sort); + System.out.println(sList); + + } finally { + airportRepository + .deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + @Test void countSlicePage() { airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll(); @@ -1113,7 +1230,21 @@ void findBySimplePropertyAudited() { Airport saved = airportRepository.save(vie); List airports1 = airportRepository.findAllByIata("vie"); assertEquals(saved, airports1.get(0)); - assertEquals(saved.getCreatedBy(), NaiveAuditorAware.AUDITOR); // NaiveAuditorAware will provide this + assertEquals(NaiveAuditorAware.AUDITOR, saved.getCreatedBy()); // NaiveAuditorAware will provide this + } finally { + airportRepository.delete(vie); + } + } + + @Test + void findBySimplePropertyAuditedReactive() { + Airport vie = null; + try { + vie = new Airport("airports::vie", "vie", "low2"); + Airport saved = reactiveAirportRepository.save(vie).block(); + List airports1 = airportRepository.findAllByIata("vie"); + assertEquals(saved, airports1.get(0)); + assertEquals(ReactiveNaiveAuditorAware.AUDITOR, saved.getCreatedBy()); // ReactiveNaiveAuditorAware will provide this } finally { airportRepository.delete(vie); } @@ -1183,47 +1314,9 @@ private void sleep(int millis) { @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Bean(name = "auditorAwareRef") - public NaiveAuditorAware testAuditorAware() { - return new NaiveAuditorAware(); - } - - @Override - public void configureEnvironment(final ClusterEnvironment.Builder builder) { - // builder.ioConfig().maxHttpConnections(11).idleHttpConnectionTimeout(Duration.ofSeconds(4)); - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - @Bean(name = "dateTimeProviderRef") - public DateTimeProvider testDateTimeProvider() { - return new AuditingDateTimeProvider(); - } + @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + @EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + static class Config extends org.springframework.data.couchbase.domain.Config { @Bean public LocalValidatorFactoryBean validator() { @@ -1239,46 +1332,8 @@ public ValidatingCouchbaseEventListener validationEventListener() { @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") - static class ConfigRequestPlus extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - @Bean(name = "auditorAwareRef") - public NaiveAuditorAware testAuditorAware() { - return new NaiveAuditorAware(); - } - - @Bean(name = "dateTimeProviderRef") - public DateTimeProvider testDateTimeProvider() { - return new AuditingDateTimeProvider(); - } - + @EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + static class ConfigRequestPlus extends org.springframework.data.couchbase.domain.Config { @Override public QueryScanConsistency getDefaultConsistency() { return REQUEST_PLUS; diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index 0e964bc4a..d5dd4f305 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -34,6 +34,7 @@ import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.ReactiveAirlineRepository; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; @@ -45,6 +46,7 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -54,7 +56,8 @@ /** * @author Michael Reiche */ -@SpringJUnitConfig(ReactiveCouchbaseRepositoryKeyValueIntegrationTests.Config.class) +@SpringJUnitConfig(Config.class) +@DirtiesContext @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareIntegrationTests { @@ -117,49 +120,4 @@ void findByIdAudited() { } } - @Configuration - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - @EnableReactiveCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - @Bean(name = "auditorAwareRef") - public ReactiveNaiveAuditorAware testAuditorAware() { - return new ReactiveNaiveAuditorAware(); - } - - @Bean(name = "dateTimeProviderRef") - public DateTimeProvider testDateTimeProvider() { - return new AuditingDateTimeProvider(); - } - - } - } diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 42560e8f1..cda628093 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -39,16 +41,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveUserRepository; import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -56,9 +55,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.java.env.ClusterEnvironment; /** * template class for Reactive Couchbase operations @@ -66,7 +62,8 @@ * @author Michael Nitschinger * @author Michael Reiche */ -@SpringJUnitConfig(ReactiveCouchbaseRepositoryQueryIntegrationTests.Config.class) +@SpringJUnitConfig(Config.class) +@DirtiesContext @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegrationTests { @@ -96,6 +93,16 @@ void shouldSaveAndFindAll() { } } + @Test + void testPrimitiveArgs() { + int iint = 0; + long llong = 0; + double ddouble = 0.0; + boolean bboolean = true; + List all = reactiveAirportRepository.withScope("_default") + .findAllTestPrimitives(iint, llong, ddouble, bboolean).toStream().collect(Collectors.toList()); + } + @Test void testQuery() { Airport vie = null; @@ -295,37 +302,4 @@ void deleteOne() { } } - @Configuration - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - } - } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryCollectionQuerydslIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryCollectionQuerydslIntegrationTests.java new file mode 100644 index 000000000..bc5ea6bad --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryCollectionQuerydslIntegrationTests.java @@ -0,0 +1,631 @@ +/* + * Copyright 2017-2025 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.couchbase.repository.query; + +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.domain.*; +import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.auditing.EnableReactiveCouchbaseAuditing; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.support.BasicQuery; +import org.springframework.data.couchbase.repository.support.SpringDataCouchbaseSerializer; +import org.springframework.data.couchbase.util.*; +import org.springframework.data.domain.Sort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.StreamSupport; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.data.couchbase.util.Util.comprises; +import static org.springframework.data.couchbase.util.Util.exactly; + +/** + * Repository tests + * + * @author Tigran Babloyan + */ +@SpringJUnitConfig(CouchbaseRepositoryCollectionQuerydslIntegrationTests.Config.class) +@DirtiesContext +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +public class CouchbaseRepositoryCollectionQuerydslIntegrationTests extends CollectionAwareDefaultScopeIntegrationTests { + + @Autowired + AirlineCollectionedRepository airlineRepository; + + static QAirlineCollectioned airline = QAirlineCollectioned.airlineCollectioned; + // saved + static AirlineCollectioned united = new AirlineCollectioned("1", "United Airlines", "US"); + static AirlineCollectioned lufthansa = new AirlineCollectioned("2", "Lufthansa", "DE"); + static AirlineCollectioned emptyStringAirline = new AirlineCollectioned("3", "Empty String", ""); + static AirlineCollectioned nullStringAirline = new AirlineCollectioned("4", "Null String", null); + static AirlineCollectioned unitedLowercase = new AirlineCollectioned("5", "united airlines", "US"); + static AirlineCollectioned[] saved = new AirlineCollectioned[] { united, lufthansa, emptyStringAirline, nullStringAirline, unitedLowercase }; + // not saved + static AirlineCollectioned flyByNight = new AirlineCollectioned("1001", "Fly By Night", "UK"); + static AirlineCollectioned sleepByDay = new AirlineCollectioned("1002", "Sleep By Day", "CA"); + static AirlineCollectioned[] notSaved = new AirlineCollectioned[] { flyByNight, sleepByDay }; + + @Autowired CouchbaseTemplate couchbaseTemplate; + + SpringDataCouchbaseSerializer serializer = null; + + @BeforeEach + public void beforeEach() { + super.beforeEach(); + serializer = new SpringDataCouchbaseSerializer(couchbaseTemplate.getConverter()); + } + + @BeforeAll + static public void beforeAll() { + callSuperBeforeAll(new Object() {}); + ApplicationContext ac = new AnnotationConfigApplicationContext( + CouchbaseRepositoryCollectionQuerydslIntegrationTests.Config.class); + CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate"); + for (AirlineCollectioned airline : saved) { + template.insertById(AirlineCollectioned.class).one(airline); + } + template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + logDisconnect(template.getCouchbaseClientFactory().getCluster(), "queryDsl-before"); + } + + @AfterAll + static public void afterAll() { + ApplicationContext ac = new AnnotationConfigApplicationContext( + CouchbaseRepositoryCollectionQuerydslIntegrationTests.Config.class); + CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate"); + for (AirlineCollectioned airline : saved) { + template.removeById(AirlineCollectioned.class).one(airline.getId()); + } + template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + logDisconnect(template.getCouchbaseClientFactory().getCluster(), "queryDsl-after"); + callSuperAfterAll(new Object() {}); + } + + @Test + void testEq() { + { + BooleanExpression predicate = airline.name.eq(flyByNight.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(flyByNight.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + + } + { + BooleanExpression predicate = airline.name.eq(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + } + + // this gives hqCountry == "" and hqCountry is missing + // @Test + void testStringIsEmpty() { + { + BooleanExpression predicate = airline.hqCountry.isEmpty(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, emptyStringAirline, nullStringAirline), "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) like $1", bq(predicate)); + } + } + + @Test + void testNot() { + { + BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry())) + .not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> !(a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry()))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( ( (hqCountry = $1) and (name = $2)) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName())).not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name = $1) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.eq(united.getName()).not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name = $1) )", bq(predicate)); + } + } + + @Test + void testAnd() { + { + BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry())) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.eq(united.getName()) + .and(airline.hqCountry.eq(lufthansa.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(lufthansa.getHqCountry())) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate)); + } + } + + @Test + void testOr() { + { + BooleanExpression predicate = airline.name.eq(united.getName()) + .or(airline.hqCountry.eq(lufthansa.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName())) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) or (hqCountry = $2)", bq(predicate)); + } + } + + @Test + void testNe() { + { + BooleanExpression predicate = airline.name.ne(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name != $1", bq(predicate)); + } + } + + @Test + void testStartsWith() { + { + BooleanExpression predicate = airline.name.startsWith(united.getName().substring(0, 5)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved) + .filter(a -> a.getName().startsWith(united.getName().substring(0, 5))).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name like ($1||\"%\")", bq(predicate)); + } + } + + @Test + void testStartsWithIgnoreCase() { + { + BooleanExpression predicate = airline.name.startsWithIgnoreCase(united.getName().substring(0, 5)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase().startsWith(united.getName().toUpperCase().substring(0, 5))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE lower(name) like ($1||\"%\")", bq(predicate)); + } + } + + @Test + void testEndsWith() { + { + BooleanExpression predicate = airline.name.endsWith(united.getName().substring(1)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getName().endsWith(united.getName().substring(1))) + .toArray(AirlineCollectioned[]::new)), "[unexpected] -> [missing]"); + assertEquals(" WHERE name like (\"%\"||$1)", bq(predicate)); + + } + } + + @Test + void testEndsWithIgnoreCase() { + { + BooleanExpression predicate = airline.name.endsWithIgnoreCase(united.getName().substring(1)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase().endsWith(united.getName().toUpperCase().substring(1))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE lower(name) like (\"%\"||$1)", bq(predicate)); + } + } + + @Test + void testEqIgnoreCase() { + { + BooleanExpression predicate = airline.name.equalsIgnoreCase(flyByNight.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(flyByNight.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE lower(name) = $1", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.equalsIgnoreCase(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(united.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE lower(name) = $1", bq(predicate)); + } + + } + + @Test + void testContains() { + { + BooleanExpression predicate = airline.name.contains("United"); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("United")).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE contains(name, $1)", bq(predicate)); + } + } + + @Test + void testContainsIgnoreCase() { + { + BooleanExpression predicate = airline.name.containsIgnoreCase("united"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase(Locale.ROOT).contains("united".toUpperCase(Locale.ROOT))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE contains(lower(name), $1)", bq(predicate)); + + } + } + + @Test + void testLike() { + { + BooleanExpression predicate = airline.name.like("%nited%"); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("nited")).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name like $1", bq(predicate)); + } + } + + @Test + void testLikeIgnoreCase() { + { + BooleanExpression predicate = airline.name.likeIgnoreCase("%Airlines"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase(Locale.ROOT).endsWith("Airlines".toUpperCase(Locale.ROOT))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE lower(name) like $1", bq(predicate)); + } + } + + // This is 'between' is inclusive + @Test + void testBetween() { + { + BooleanExpression predicate = airline.name.between(flyByNight.getName(), united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter( + a -> a.getName().compareTo(flyByNight.getName()) >= 0 && a.getName().compareTo(united.getName()) <= 0) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name between $1 and $2", bq(predicate)); + } + } + + @Test + void testIn() { + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName())); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName(), lufthansa.getName())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName())) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name in $1", bq(predicate)); + } + + { + BooleanExpression predicate = airline.name.in("Fly By Night", "Sleep By Day"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName())) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name in $1", bq(predicate)); + } + } + + @Test + void testSort(){ + { + BooleanExpression predicate = airline.name.in(Arrays.stream(saved).map(AirlineCollectioned::getName).toList()); + Iterable result = airlineRepository.findAll(predicate, Sort.by("name").ascending()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getName)) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + + { + BooleanExpression predicate = airline.name.in(Arrays.stream(saved).map(AirlineCollectioned::getName).toList()); + Iterable result = airlineRepository.findAll(predicate, Sort.by("name").descending()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getName).reversed()) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + + { + BooleanExpression predicate = airline.name.in(Arrays.stream(saved).map(AirlineCollectioned::getName).toList()); + Iterable result = airlineRepository.findAll(predicate, airline.name.asc()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getName)) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + + { + BooleanExpression predicate = airline.name.in(Arrays.stream(saved).map(AirlineCollectioned::getName).toList()); + Iterable result = airlineRepository.findAll(predicate, airline.name.desc()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getName).reversed()) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + + { + Comparator nullSafeStringComparator = Comparator + .nullsFirst(String::compareTo); + Iterable result = airlineRepository.findAll(airline.hqCountry.asc().nullsFirst()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getHqCountry, nullSafeStringComparator)) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + + { + Comparator nullSafeStringComparator = Comparator + .nullsFirst(String::compareTo); + Iterable result = airlineRepository.findAll(airline.hqCountry.desc().nullsLast()); + assertArrayEquals(StreamSupport.stream(result.spliterator(), false).toArray(AirlineCollectioned[]::new), + Arrays.stream(saved) + .sorted(Comparator.comparing(AirlineCollectioned::getHqCountry, nullSafeStringComparator).reversed()) + .toArray(AirlineCollectioned[]::new), + "Order of airlines does not match"); + } + } + + + @Test + void testNotIn() { + { + BooleanExpression predicate = airline.name.notIn("Fly By Night", "Sleep By Day"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> !(a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName()))) + .toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name in $1) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.notIn(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name != $1", bq(predicate)); + } + } + + @Test + @Disabled + void testColIsEmpty() {} + + @Test + void testLt() { + { + BooleanExpression predicate = airline.name.lt(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) < 0).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name < $1", bq(predicate)); + } + } + + @Test + void testGt() { + { + BooleanExpression predicate = airline.name.gt(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) > 0).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name > $1", bq(predicate)); + } + } + + @Test + void testLoe() { + { + BooleanExpression predicate = airline.name.loe(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) <= 0).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name <= $1", bq(predicate)); + } + } + + @Test + void testGoe() { + { + BooleanExpression predicate = airline.name.goe(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) >= 0).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name >= $1", bq(predicate)); + } + } + + // when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull + // and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull + // so isNull and isNotNull are *not* compliments + @Test + @Disabled + void testIsNull() { + { + BooleanExpression predicate = airline.hqCountry.isNull(); + Optional result = airlineRepository.findOne(predicate); + assertNull(exactly(result, nullStringAirline), "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + } + + // when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull + // and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull + // so isNull and isNotNull are *not* compliments + @Test + void testIsNotNull() { + { + BooleanExpression predicate = airline.hqCountry.isNotNull(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getHqCountry() != null).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE hqCountry is not null", bq(predicate)); + } + } + + @Test + @Disabled + void testContainsKey() {} + + @Test + void testStringLength() { + { + BooleanExpression predicate = airline.name.length().eq(united.getName().length()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().length() == united.getName().length()).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE LENGTH(name) = $1", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.length().eq(flyByNight.getName().length()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved) + .filter(a -> a.getName().length() == flyByNight.getName().length()).toArray(AirlineCollectioned[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE LENGTH(name) = $1", bq(predicate)); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + @EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + + static class Config extends org.springframework.data.couchbase.domain.Config { + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + @Bean + public ValidatingCouchbaseEventListener validationEventListener() { + return new ValidatingCouchbaseEventListener(validator()); + } + } + + String bq(Predicate predicate) { + BasicQuery basicQuery = new BasicQuery((QueryCriteriaDefinition) serializer.handle(predicate), null); + return basicQuery.export(new int[1]); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 7aed5d2e6..83b8fac85 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -17,6 +17,8 @@ import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,6 +30,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; @@ -36,10 +39,13 @@ import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.AddressAnnotated; +import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; import org.springframework.data.couchbase.domain.AirportRepositoryAnnotated; -import org.springframework.data.couchbase.domain.CollectionsConfig; +import org.springframework.data.couchbase.domain.BigAirline; +import org.springframework.data.couchbase.domain.BigAirlineRepository; +import org.springframework.data.couchbase.domain.ConfigScoped; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserCol; import org.springframework.data.couchbase.domain.UserColRepository; @@ -51,6 +57,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; @@ -64,11 +71,13 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CollectionsConfig.class) +@SpringJUnitConfig(ConfigScoped.class) +@DirtiesContext public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired AirportRepositoryAnnotated airportRepositoryAnnotated; @Autowired AirportRepository airportRepository; + @Autowired BigAirlineRepository bigAirlineRepository; @Autowired UserColRepository userColRepository; @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; @@ -100,6 +109,7 @@ public void beforeEach() { couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all(); couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(BigAirline.class).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName2).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); } @@ -123,6 +133,19 @@ void findByKey() { userColRepository.delete(found); } + @Test + @Disabled // BigInteger and BigDecimal lose precision through Query + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) + void saveBig() { + BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null); + airline = bigAirlineRepository.withCollection(collectionName).save(airline); + List foundMaybe = bigAirlineRepository.withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).getByName("MyAirline"); + BigAirline found = (BigAirline) foundMaybe.get(0); + assertEquals(found, airline); + bigAirlineRepository.withCollection(collectionName).delete(airline); + } + @Test public void myTest() { @@ -422,4 +445,23 @@ void stringDeleteWithMethodAnnotationTest() { } } + + @Test // DATACOUCH-650, SDC-1939 + void deleteAllById() { + + Airport vienna = new Airport("airports::vie", "vie", "LOWW"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDZ"); + Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + try { + ar.saveAll(asList(vienna, frankfurt, losAngeles)); + List airports = ar.findAllById(asList(vienna.getId(), losAngeles.getId())); + assertEquals(2, airports.size()); + ar.deleteAllById(asList(vienna.getId(), losAngeles.getId())); + assertThat(ar.findAll()).containsExactly(frankfurt); + ar.deleteAll(asList(frankfurt)); + } finally { + ar.deleteAll(); + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java index 18c56438a..5bc751824 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -17,9 +17,10 @@ package org.springframework.data.couchbase.repository.query; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.data.couchbase.util.Util.comprises; import static org.springframework.data.couchbase.util.Util.exactly; @@ -29,8 +30,10 @@ import java.util.Optional; import java.util.stream.StreamSupport; +import com.querydsl.core.types.dsl.PathBuilder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,17 +41,14 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.auditing.DateTimeProvider; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener; import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.AirlineRepository; -import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.QAirline; -import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.auditing.EnableReactiveCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.repository.support.BasicQuery; import org.springframework.data.couchbase.repository.support.SpringDataCouchbaseSerializer; @@ -57,13 +57,10 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.data.domain.Sort; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.client.java.query.QueryScanConsistency; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; @@ -74,6 +71,7 @@ * @author Tigran Babloyan */ @SpringJUnitConfig(CouchbaseRepositoryQuerydslIntegrationTests.Config.class) +@DirtiesContext @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryQuerydslIntegrationTests extends JavaIntegrationTests { @@ -92,7 +90,15 @@ public class CouchbaseRepositoryQuerydslIntegrationTests extends JavaIntegration static Airline sleepByDay = new Airline("1002", "Sleep By Day", "CA"); static Airline[] notSaved = new Airline[] { flyByNight, sleepByDay }; - SpringDataCouchbaseSerializer serializer = new SpringDataCouchbaseSerializer(couchbaseTemplate.getConverter()); + @Autowired CouchbaseTemplate couchbaseTemplate; + + SpringDataCouchbaseSerializer serializer = null; + + @BeforeEach + public void beforeEach() { + super.beforeEach(); + serializer = new SpringDataCouchbaseSerializer(couchbaseTemplate.getConverter()); + } @BeforeAll static public void beforeAll() { @@ -104,6 +110,7 @@ static public void beforeAll() { template.insertById(Airline.class).one(airline); } template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + logDisconnect(template.getCouchbaseClientFactory().getCluster(), "queryDsl-before"); } @AfterAll @@ -115,6 +122,7 @@ static public void afterAll() { template.removeById(Airline.class).one(airline.getId()); } template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + logDisconnect(template.getCouchbaseClientFactory().getCluster(), "queryDsl-after"); callSuperAfterAll(new Object() {}); } @@ -141,6 +149,13 @@ void testEq() { } } + @Test + void testInjection() { + String userSpecifiedPath = "1 = 1) OR (2"; + PathBuilder pathBuilder = new PathBuilder<>(QAirline.class, "xyz"); + assertThrows(IllegalStateException.class, () -> pathBuilder.get(userSpecifiedPath).eq("2")); + } + // this gives hqCountry == "" and hqCountry is missing // @Test void testStringIsEmpty() { @@ -415,7 +430,7 @@ void testIn() { assertEquals(" WHERE name in $1", bq(predicate)); } } - + @Test void testSort(){ { @@ -480,7 +495,7 @@ void testSort(){ "Order of airlines does not match"); } } - + @Test void testNotIn() { @@ -611,56 +626,12 @@ void testStringLength() { } } - private void sleep(int millis) { - try { - Thread.sleep(millis); // so they are executed out-of-order - } catch (InterruptedException ie) { - ; - } - } - @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Bean(name = "auditorAwareRef") - public NaiveAuditorAware testAuditorAware() { - return new NaiveAuditorAware(); - } - - @Override - public void configureEnvironment(final ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } + @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + @EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") - @Bean(name = "dateTimeProviderRef") - public DateTimeProvider testDateTimeProvider() { - return new AuditingDateTimeProvider(); - } + static class Config extends org.springframework.data.couchbase.domain.Config { @Bean public LocalValidatorFactoryBean validator() { @@ -678,52 +649,4 @@ String bq(Predicate predicate) { return basicQuery.export(new int[1]); } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") - static class ConfigRequestPlus extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - @Bean(name = "auditorAwareRef") - public NaiveAuditorAware testAuditorAware() { - return new NaiveAuditorAware(); - } - - @Bean(name = "dateTimeProviderRef") - public DateTimeProvider testDateTimeProvider() { - return new AuditingDateTimeProvider(); - } - - @Override - public QueryScanConsistency getDefaultConsistency() { - return REQUEST_PLUS; - } - } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java index 8167adf5c..b47910b09 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -25,6 +25,8 @@ import java.util.List; import java.util.Locale; +import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ArrayNode; +import com.couchbase.client.java.query.QueryOptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -32,6 +34,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; @@ -44,6 +47,7 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.PartTree; @@ -119,18 +123,17 @@ void queryParametersArray() throws Exception { QueryMethod queryMethod = new QueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory()); Query expected = (new Query()).addCriteria(where(i("firstname")).in("Oliver", "Charles")); + JsonArray parameters = JsonArray.create().add(JsonArray.create().add("Oliver").add("Charles")); + QueryOptions expectedQOptions = QueryOptions.queryOptions().parameters(parameters); N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), new Object[] { new Object[] { "Oliver", "Charles" } }), queryMethod, converter, bucketName); Query query = creator.createQuery(); - // Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); assertEquals(" WHERE `firstname` in $1", query.export(new int[1])); - JsonObject expectedOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(expectedOptions); - JsonObject actualOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(actualOptions); - assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + ArrayNode expectedOptions = expected.buildQueryOptions(expectedQOptions, null).build().positionalParameters(); + ArrayNode actualOptions = query.buildQueryOptions(null, null).build().positionalParameters(); + assertEquals(expectedOptions.toString(), actualOptions.toString()); } @Test @@ -148,12 +151,12 @@ void queryParametersJsonArray() throws Exception { Query query = creator.createQuery(); Query expected = (new Query()).addCriteria(where(i("firstname")).in("Oliver", "Charles")); + JsonArray parameters = JsonArray.create().add(JsonArray.create().add("Oliver").add("Charles")); + QueryOptions expectedQOptions = QueryOptions.queryOptions().parameters(parameters); assertEquals(" WHERE `firstname` in $1", query.export(new int[1])); - JsonObject expectedOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(expectedOptions); - JsonObject actualOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(actualOptions); - assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + ArrayNode expectedOptions = expected.buildQueryOptions(expectedQOptions, null).build().positionalParameters(); + ArrayNode actualOptions = query.buildQueryOptions(null, null).build().positionalParameters(); + assertEquals(expectedOptions.toString(), actualOptions.toString()); } @Test @@ -171,13 +174,13 @@ void queryParametersList() throws Exception { Query query = creator.createQuery(); Query expected = (new Query()).addCriteria(where(i("firstname")).in("Oliver", "Charles")); + JsonArray parameters = JsonArray.create().add(JsonArray.create().add("Oliver").add("Charles")); + QueryOptions expectedQOptions = QueryOptions.queryOptions().parameters(parameters); assertEquals(" WHERE `firstname` in $1", query.export(new int[1])); - JsonObject expectedOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(expectedOptions); - JsonObject actualOptions = JsonObject.create(); - expected.buildQueryOptions(null, null).build().injectParams(actualOptions); - assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + ArrayNode expectedOptions = expected.buildQueryOptions(expectedQOptions, null).build().positionalParameters(); + ArrayNode actualOptions = query.buildQueryOptions(null, null).build().positionalParameters(); + assertEquals(expectedOptions.toString(), actualOptions.toString()); } @Test @@ -230,7 +233,7 @@ private ParameterAccessor getAccessor(Parameters params, Object... values) } private Parameters getParameters(Method method) { - return new DefaultParameters(method); + return new DefaultParameters(ParametersSource.of(method)); } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 6f41d2070..c98bb4547 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -15,10 +15,17 @@ */ package org.springframework.data.couchbase.repository.query; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.springframework.data.couchbase.domain.AirportRepository; +import reactor.core.Disposable; + import java.util.List; +import java.util.Random; +import java.util.UUID; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -30,7 +37,8 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Airport; -import org.springframework.data.couchbase.domain.CollectionsConfig; +import org.springframework.data.couchbase.domain.ConfigScoped; +import org.springframework.data.couchbase.domain.ReactiveAirportMustScopeRepository; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveAirportRepositoryAnnotated; import org.springframework.data.couchbase.domain.ReactiveUserColRepository; @@ -40,6 +48,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; @@ -53,12 +62,14 @@ * * @author Michael Reiche */ -@SpringJUnitConfig(CollectionsConfig.class) +@SpringJUnitConfig(ConfigScoped.class) @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@DirtiesContext public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired ReactiveAirportRepository reactiveAirportRepository; @Autowired ReactiveAirportRepositoryAnnotated reactiveAirportRepositoryAnnotated; + @Autowired ReactiveAirportMustScopeRepository reactiveAirportMustScopeRepository; @Autowired ReactiveUserColRepository userColRepository; @Autowired public CouchbaseTemplate couchbaseTemplate; @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @@ -114,6 +125,21 @@ public void myTest() { } + @Test + void testThreadLocal() throws InterruptedException { + + String scopeName = "my_scope"; + String id = UUID.randomUUID().toString(); + + Airport airport = new Airport(id, "testThreadLocal", "icao"); + Disposable s = reactiveAirportMustScopeRepository.withScope(scopeName).findById(airport.getId()).doOnNext(u -> { + throw new RuntimeException("User already Exists! " + u); + }).then(reactiveAirportMustScopeRepository.withScope(scopeName).save(airport)) + .subscribe(u -> LOGGER.info("User Persisted Successfully! {}", u)); + + reactiveAirportMustScopeRepository.withScope(scopeName).deleteById(id).block(); + } + /** * can test against _default._default without setting up additional scope/collection and also test for collections and * scopes that do not exist These same tests should be repeated on non-default scope and collection in a test that @@ -275,4 +301,23 @@ void stringDeleteWithMethodAnnotationTest() { } } + @Test // DATACOUCH-650, SDC-1939 + void deleteAllById() { + + Airport vienna = new Airport("airports::vie", "vie", "LOWW"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDZ"); + Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); + ReactiveAirportRepository ar = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName); + try { + ar.saveAll(asList(vienna, frankfurt, losAngeles)).blockLast(); + List airports = ar.findAllById(asList(vienna.getId(), losAngeles.getId())).collectList().block(); + assertEquals(2, airports.size()); + ar.deleteAllById(asList(vienna.getId(), losAngeles.getId())).block(); + assertThat(ar.findAll().collectList().block()).containsExactly(frankfurt); + ar.deleteAll(asList(frankfurt)).block(); + } finally { + ar.deleteAll().block(); + } + } + } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java index 3e35d8699..37a0b02d3 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -15,18 +15,16 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Method; import java.util.Optional; import java.util.Properties; import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation.ExecutableFindByQuery; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -35,7 +33,7 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.AirlineRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; @@ -49,21 +47,20 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.query.QueryScanConsistency; /** * @author Michael Nitschinger * @author Michael Reiche */ -@SpringJUnitConfig(StringN1qlQueryCreatorIntegrationTests.Config.class) +@SpringJUnitConfig(Config.class) @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@DirtiesContext class StringN1qlQueryCreatorIntegrationTests extends ClusterAwareIntegrationTests { @Autowired MappingContext, CouchbasePersistentProperty> context; @@ -71,9 +68,6 @@ class StringN1qlQueryCreatorIntegrationTests extends ClusterAwareIntegrationTest @Autowired CouchbaseTemplate couchbaseTemplate; static NamedQueries namedQueries = new PropertiesBasedNamedQueries(new Properties()); - @BeforeEach - public void beforeEach() {} - @Test @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) void findUsingStringNq1l() throws Exception { @@ -89,7 +83,7 @@ void findUsingStringNq1l() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); Query query = creator.createQuery(); @@ -122,7 +116,7 @@ void findUsingStringNq1l_3x_projection_id_cas() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); Query query = creator.createQuery(); @@ -145,45 +139,13 @@ private ParameterAccessor getAccessor(Parameters params, Object... values) } private Parameters getParameters(Method method) { - return new DefaultParameters(method); + return new DefaultParameters(ParametersSource.of(method)); } - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - protected void configureEnvironment(ClusterEnvironment.Builder builder) { - if (config().isUsingCloud()) { - builder.securityConfig( - SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)); - } - } - - @Override - protected boolean autoIndexCreation() { - return true; - } - - } + // static class Config extends org.springframework.data.couchbase.domain.Config { + // @Override + // protected boolean autoIndexCreation() { + // return true; + // } + // } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index f7ca81176..0d1b5fff2 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2025 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. @@ -15,20 +15,21 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Method; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.StringQuery; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.mapping.context.MappingContext; @@ -40,8 +41,8 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Michael Nitschinger @@ -70,7 +71,7 @@ void wrongNumberArgs() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); } catch (IllegalArgumentException e) { return; @@ -88,7 +89,7 @@ void doesNotHaveAnnotation() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); } catch (IllegalArgumentException e) { return; @@ -106,11 +107,11 @@ void createsQueryCorrectly() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); Query query = creator.createQuery(); assertEquals( - "SELECT `_class`, META(`" + bucketName() + "SELECT `_class`, `jsonNode`, `jsonObject`, `jsonArray`, META(`" + bucketName() + "`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`" + bucketName() + "`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `" + bucketName() + "` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2", @@ -127,17 +128,70 @@ void createsQueryCorrectly2() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, ValueExpressionDelegate.create(), namedQueries); Query query = creator.createQuery(); assertEquals( - "SELECT `_class`, META(`" + bucketName() + "SELECT `_class`, `jsonNode`, `jsonObject`, `jsonArray`, META(`" + bucketName() + "`).`cas` AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`" + bucketName() + "`).`id` AS __id, `firstname`, `lastname`, `subtype` FROM `" + bucketName() + "` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)", query.toN1qlSelectString(converter, bucketName(), null, null, User.class, User.class, false, null, null)); } + @Test + void stringQuerycreatesQueryCorrectly() throws Exception { + String queryString = "a b c"; + Query query = new StringQuery(queryString); + assertEquals(queryString, query.toN1qlSelectString(converter, bucketName(), null, null, User.class, User.class, + false, null, null)); + } + + @Test + void stringQueryNoPositionalParameters() { + String queryString = " $1"; + Query query = new StringQuery(queryString); + assertThrows(IllegalArgumentException.class, () -> query.toN1qlSelectString(converter, bucketName(), null, null, + User.class, User.class, false, null, null)); + } + + @Test + void stringQueryNoNamedParameters() { + String queryString = " $george"; + Query query = new StringQuery(queryString); + assertThrows(IllegalArgumentException.class, () -> query.toN1qlSelectString(converter, bucketName(), null, null, + User.class, User.class, false, null, null)); + } + + @Test + void stringQueryNoSpelExpressions() { + String queryString = "#{#n1ql.filter}"; + Query query = new StringQuery(queryString); + assertThrows(IllegalArgumentException.class, () -> query.toN1qlSelectString(converter, bucketName(), null, null, + User.class, User.class, false, null, null)); + } + + @Test + void stringQueryNoPositionalParametersQuotes() { + String queryString = " '$1'"; + Query query = new StringQuery(queryString); + query.toN1qlSelectString(converter, bucketName(), null, null, User.class, User.class, false, null, null); + } + + @Test + void stringQueryNoNamedParametersQuotes() { + String queryString = " '$george'"; + Query query = new StringQuery(queryString); + query.toN1qlSelectString(converter, bucketName(), null, null, User.class, User.class, false, null, null); + } + + @Test + void stringQueryNoSpelExpressionsQuotes() { + String queryString = "'#{#n1ql.filter}'"; + Query query = new StringQuery(queryString); + query.toN1qlSelectString(converter, bucketName(), null, null, User.class, User.class, false, null, null); + } + @Test void spelTests() throws Exception { String input = "spelTests"; @@ -147,15 +201,13 @@ void spelTests() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method)), queryMethod, - converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + converter, ValueExpressionDelegate.create(), namedQueries); Query query = creator.createQuery(); - assertEquals( - "SELECT `_class`, META(`myCollection`).`cas` AS __cas, `createdBy`, `createdDate`, " - + "`lastModifiedBy`, `lastModifiedDate`, META(`myCollection`).`id` AS __id, `firstname`, " - + "`lastname`, `subtype` FROM `myCollection`|`_class` = \"abstractuser\"" - + "|`myCollection`|`myScope`|`myCollection`", + assertEquals("SELECT `_class`, `jsonNode`, `jsonObject`, `jsonArray`, META(`myCollection`).`cas`" + + " AS __cas, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, META(`myCollection`).`id`" + + " AS __id, `firstname`, `lastname`, `subtype` FROM `myCollection`|`_class` = \"abstractuser\"|`some_bucket`|`myScope`|`myCollection`", query.toN1qlSelectString(converter, bucketName(), "myScope", "myCollection", User.class, null, false, null, null)); } @@ -169,7 +221,7 @@ private ParameterAccessor getAccessor(Parameters params, Object... values) } private Parameters getParameters(Method method) { - return new DefaultParameters(method); + return new DefaultParameters(ParametersSource.of(method)); } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java index bd9857325..9728b3d74 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.transactions; -import lombok.Data; - import org.springframework.data.domain.Persistable; /** @@ -25,12 +23,16 @@ * * @author Michael Reiche */ -@Data + public class AfterTransactionAssertion { private final T persistable; private boolean expectToBePresent; + public AfterTransactionAssertion(T persistable) { + this.persistable = persistable; + } + public void isPresent() { expectToBePresent = true; } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 77aff69d4..6a1ddcc1f 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import lombok.Data; import reactor.core.publisher.Mono; import java.util.List; @@ -51,6 +50,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.reactive.TransactionalOperator; @@ -66,7 +66,9 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, PersonService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, PersonService.class }) +@DirtiesContext public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -323,7 +325,6 @@ public void replaceWithCasConflictResolvedViaRetryAnnotated() { assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); } - @Data static class EventLog { public EventLog() {}; // don't remove this @@ -343,6 +344,10 @@ public String toString() { sb.append(", action: " + action); return sb.toString(); } + + private String getId() { + return id; + } } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index 37ca01aa7..c26502fe6 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -18,8 +18,7 @@ import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -import lombok.Data; -import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.util.Util; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -37,6 +36,7 @@ import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; import org.springframework.data.couchbase.domain.ReactivePersonRepository; import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; @@ -44,6 +44,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.Cluster; @@ -58,6 +59,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(classes = { TransactionsConfig.class, PersonServiceReactive.class }) +@DirtiesContext public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -118,6 +120,7 @@ public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { @Test public void commitShouldPersistTxEntries() { + System.err.println("parent SecurityContext: " + System.identityHashCode(Util.getSecurityContext())); personService.savePerson(WalterWhite) // .as(StepVerifier::create) // .expectNextCount(1) // @@ -129,6 +132,17 @@ public void commitShouldPersistTxEntries() { .verifyComplete(); } + @Test + public void commitShouldPersistTxEntriesBlocking() { + System.err.println("parent SecurityContext: " + System.identityHashCode(Util.getSecurityContext())); + Person p = personService.savePersonBlocking(WalterWhite); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + @Test public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { @@ -215,8 +229,6 @@ public void errorAfterTxShouldNotAffectPreviousStep() { .verifyComplete(); } - @Data - // @AllArgsConstructor static class EventLog { public EventLog() {} @@ -234,4 +246,5 @@ public EventLog(String id, String action) { String action; @Version Long version; } + } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java index 909f1d502..ddd6823f3 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -55,6 +56,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) +@DirtiesContext public class CouchbaseReactiveTransactionNativeIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -228,4 +230,5 @@ public Mono savePerson(Person person) { .as(txOperator::transactional); } + static public class TransactionsConfig extends org.springframework.data.couchbase.transactions.TransactionsConfig {} } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java index d8f9b8f43..2021be224 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -41,6 +41,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.TransactionManager; import org.springframework.transaction.reactive.TransactionalOperator; @@ -54,6 +55,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) +@DirtiesContext // I think these are all redundant (see CouchbaseReactiveTransactionNativeTests). There does not seem to be a blocking // form of TransactionalOperator. Also there does not seem to be a need for a CouchbaseTransactionalOperator as // TransactionalOperator.create(reactiveCouchbaseTransactionManager) seems to work just fine. (I don't recall what @@ -185,4 +187,5 @@ public void replacePersonRbSpringTransactional() { assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be Walter"); } + static public class TransactionsConfig extends org.springframework.data.couchbase.transactions.TransactionsConfig {} } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java index 1241933e0..3d5a950a6 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -37,8 +37,8 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; /** @@ -50,6 +50,7 @@ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(classes = { TransactionsConfig.class, CouchbaseTransactionalNonAllowableOperationsIntegrationTests.PersonService.class }) +@DirtiesContext public class CouchbaseTransactionalNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -127,4 +128,5 @@ public T doInTransaction(AtomicInteger tryCount, Function callback) { callback.accept(ops); } } + } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryCollectionIntegrationTests.java index cbf3649d0..29898d176 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -39,6 +39,7 @@ import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.Transactional; @@ -48,17 +49,17 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalRepositoryCollectionIntegrationTests.UserService.class }) +@SpringJUnitConfig(classes = { TransactionsConfig.class, CouchbaseTransactionalRepositoryCollectionIntegrationTests.UserService.class }) +@DirtiesContext public class CouchbaseTransactionalRepositoryCollectionIntegrationTests extends CollectionAwareIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. - @Autowired UserColRepository userRepo; - @Autowired UserService userService; + @Autowired UserColRepository userRepo; + @Autowired UserService userService; //@Autowired CouchbaseTemplate operations; @BeforeAll public static void beforeAll() { - callSuperBeforeAll(new Object() {}); + callSuperBeforeAll(new Object() {}); } @BeforeEach @@ -121,7 +122,7 @@ public void saveRolledBack() { @Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig static class UserService { - @Autowired UserColRepository userRepo; + @Autowired UserColRepository userRepo; @Transactional public void run(Consumer callback) { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java index 6ce7d2dfc..32276cc02 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -41,8 +41,8 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; /** @@ -52,7 +52,8 @@ */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig( - classes = { TransactionsConfig.class, CouchbaseTransactionalRepositoryIntegrationTests.UserService.class }) + classes = { TransactionsConfig.class, CouchbaseTransactionalRepositoryIntegrationTests.UserService.class }) +@DirtiesContext public class CouchbaseTransactionalRepositoryIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired UserRepository userRepo; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionDefaultScopeIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionDefaultScopeIntegrationTests.java new file mode 100644 index 000000000..f3e0a0bba --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionDefaultScopeIntegrationTests.java @@ -0,0 +1,487 @@ +/* + * Copyright 2022-2025 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.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareDefaultScopeIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.error.transaction.AttemptExpiredException; + +/** + * Tests for @Transactional, using template methods (findById etc.) + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateCollectionDefaultScopeIntegrationTests.PersonService.class }) +@DirtiesContext +public class CouchbaseTransactionalTemplateCollectionDefaultScopeIntegrationTests extends CollectionAwareDefaultScopeIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + // Skip this as we just one to track TransactionContext + List pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + + List pwovr = operations.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + List pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).all(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + Person inserted = personService.doInTransaction(tryCount, (ops) -> { + return ops.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(inserted.id()); + assertEquals(inserted.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + personService.fetchAndReplace(person.id(), tryCount, (p) -> { + p.setFirstname("changed"); + return p; + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + personService.fetchAndRemove(person.id(), tryCount); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + List removed = personService.doInTransaction(tryCount, ops -> { + return ops.removeByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + assertEquals(1, removed.size()); + } + + @DisplayName("A basic golden path findByQuery should succeed (though we don't know for sure it executed transactionally)") + @Test + public void committedFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + List found = personService.doInTransaction(tryCount, ops -> { + return ops.findByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + assertEquals(1, found.size()); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger tryCount = new AtomicInteger(); + AtomicReference id = new AtomicReference<>(); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + ops.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + id.set(WalterWhite.id()); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id.get()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(p.withFirstName("changed")); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertEquals(person.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.removeById(Person.class).inScope(scopeName).inCollection(collectionName).oneEntity(p); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.removeByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.findByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + assertEquals(1, tryCount.get()); + } + + @Test + public void shouldRollbackAfterException() { + assertThrowsWithCause(() -> { + personService.insertThenThrow(); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = personService.declarativeSavePerson(WalterWhite); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void concurrentTxns() { + Runnable r = () -> { + Thread t = Thread.currentThread(); + System.out.printf("Started thread %d %s%n", t.getId(), t.getName()); + Person p = personService.declarativeSavePersonWithThread(WalterWhite, t); + System.out.printf("Finished thread %d %s%n", t.getId(), t.getName()); + }; + List threads = new ArrayList<>(); + for (int i = 0; i < 50; i++) { // somewhere between 50-80 it starts to hang + Thread t = new Thread(r); + t.start(); + threads.add(t); + } + + threads.forEach(t -> { + try { + System.out.printf("Waiting for thread %d %s%n", t.getId(), t.getName()); + t.join(); + System.out.printf("Finished waiting for thread %d %s%n", t.getId(), t.getName()); + } catch (InterruptedException e) { + fail(); // interrupted + } + }); + } + + @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.") + @Test + public void replacePerson() { + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + Person refetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + operations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(refetched); + assertNotEquals(person.getVersion(), refetched.getVersion()); + AtomicInteger tryCount = new AtomicInteger(0); + assertThrowsWithCause(() -> personService.replace(person, tryCount), TransactionSystemUnambiguousException.class, + AttemptExpiredException.class); + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(person); + + assertThrowsWithCause(() -> personService.replaceEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person("Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(person); + + assertThrowsWithCause(() -> personService.removeEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("removeById().inScope(scopeName).inCollection(collectionName).one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.removeById(Person.class).inScope(scopeName).inCollection(collectionName).one(p.id()); + return p; + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig + static class PersonService { + final CouchbaseOperations personOperations; + final ReactiveCouchbaseOperations personOperationsRx; + + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx) { + personOperations = ops; + personOperationsRx = opsRx; + } + + @Transactional + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out + .println(String.format("Thread %d %s", Thread.currentThread().getId(), Thread.currentThread().getName())); + Person ret = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + System.out.println(String.format("Thread %d (was %d) %s", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName())); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public Person declarativeSavePersonWithThread(Person person, Thread thread) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out.printf("Thread %d %s, started from %d %s%n", Thread.currentThread().getId(), + Thread.currentThread().getName(), thread.getId(), thread.getName()); + Person ret = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + System.out.printf("Thread %d (was %d) %s, started from %d %s%n", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName(), thread.getId(), thread.getName()); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public void insertThenThrow() { + assertInAnnotationTransaction(true); + Person person = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(new Person("Walter", "White")); + SimulateFailureException.throwEx(); + } + + @Autowired CouchbaseCallbackTransactionManager callbackTm; + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional + public Person replacePerson(Person person, AtomicInteger tryCount) { + tryCount.incrementAndGet(); + // Note that passing in a Person and replace it in this way, is not supported + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + } + + @Transactional + public void replaceEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.replaceById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(fetched); + } + + @Transactional + public void removeEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.removeById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).oneEntity(fetched); + } + + @Transactional + public Person declarativeFindReplaceTwicePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + Person pUpdated = personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(p); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(pUpdated); + } + + @Transactional(timeout = 2) + + public Person replace(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + } + + @Transactional + public Person fetchAndReplace(String id, AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id); + Person modified = callback.apply(p); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(modified); + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + + @Transactional + public void fetchAndRemove(String id, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.removeById(Person.class).inScope(scopeName).inCollection(collectionName).oneEntity(p); + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionIntegrationTests.java new file mode 100644 index 000000000..67cc78630 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateCollectionIntegrationTests.java @@ -0,0 +1,487 @@ +/* + * Copyright 2022-2025 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.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.error.transaction.AttemptExpiredException; + +/** + * Tests for @Transactional, using template methods (findById etc.) + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateCollectionIntegrationTests.PersonService.class }) +@DirtiesContext +public class CouchbaseTransactionalTemplateCollectionIntegrationTests extends CollectionAwareIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + // Skip this as we just one to track TransactionContext + List pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + + List pwovr = operations.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).all(); + List pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).all(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + Person inserted = personService.doInTransaction(tryCount, (ops) -> { + return ops.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(inserted.id()); + assertEquals(inserted.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + personService.fetchAndReplace(person.id(), tryCount, (p) -> { + p.setFirstname("changed"); + return p; + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + personService.fetchAndRemove(person.id(), tryCount); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + List removed = personService.doInTransaction(tryCount, ops -> { + return ops.removeByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + assertEquals(1, removed.size()); + } + + @DisplayName("A basic golden path findByQuery should succeed (though we don't know for sure it executed transactionally)") + @Test + public void committedFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + List found = personService.doInTransaction(tryCount, ops -> { + return ops.findByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + assertEquals(1, found.size()); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger tryCount = new AtomicInteger(); + AtomicReference id = new AtomicReference<>(); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + ops.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + id.set(WalterWhite.id()); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id.get()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(p.withFirstName("changed")); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertEquals(person.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.removeById(Person.class).inScope(scopeName).inCollection(collectionName).oneEntity(p); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.removeByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.findByQuery(Person.class).inScope(scopeName).inCollection(collectionName).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + assertEquals(1, tryCount.get()); + } + + @Test + public void shouldRollbackAfterException() { + assertThrowsWithCause(() -> { + personService.insertThenThrow(); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = personService.declarativeSavePerson(WalterWhite); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void concurrentTxns() { + Runnable r = () -> { + Thread t = Thread.currentThread(); + System.out.printf("Started thread %d %s%n", t.getId(), t.getName()); + Person p = personService.declarativeSavePersonWithThread(WalterWhite, t); + System.out.printf("Finished thread %d %s%n", t.getId(), t.getName()); + }; + List threads = new ArrayList<>(); + for (int i = 0; i < 50; i++) { // somewhere between 50-80 it starts to hang + Thread t = new Thread(r); + t.start(); + threads.add(t); + } + + threads.forEach(t -> { + try { + System.out.printf("Waiting for thread %d %s%n", t.getId(), t.getName()); + t.join(); + System.out.printf("Finished waiting for thread %d %s%n", t.getId(), t.getName()); + } catch (InterruptedException e) { + fail(); // interrupted + } + }); + } + + @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.") + @Test + public void replacePerson() { + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + Person refetched = operations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + operations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(refetched); + assertNotEquals(person.getVersion(), refetched.getVersion()); + AtomicInteger tryCount = new AtomicInteger(0); + assertThrowsWithCause(() -> personService.replace(person, tryCount), TransactionSystemUnambiguousException.class, + AttemptExpiredException.class); + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(person); + + assertThrowsWithCause(() -> personService.replaceEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person("Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(person); + + assertThrowsWithCause(() -> personService.removeEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("removeById().inScope(scopeName).inCollection(collectionName).one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(WalterWhite); + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + ops.removeById(Person.class).inScope(scopeName).inCollection(collectionName).one(p.id()); + return p; + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @Service // this will work in the unit tests even without @Service because of explicit loading by @SpringJUnitConfig + static class PersonService { + final CouchbaseOperations personOperations; + final ReactiveCouchbaseOperations personOperationsRx; + + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx) { + personOperations = ops; + personOperationsRx = opsRx; + } + + @Transactional + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out + .println(String.format("Thread %d %s", Thread.currentThread().getId(), Thread.currentThread().getName())); + Person ret = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + System.out.println(String.format("Thread %d (was %d) %s", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName())); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public Person declarativeSavePersonWithThread(Person person, Thread thread) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out.printf("Thread %d %s, started from %d %s%n", Thread.currentThread().getId(), + Thread.currentThread().getName(), thread.getId(), thread.getName()); + Person ret = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + System.out.printf("Thread %d (was %d) %s, started from %d %s%n", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName(), thread.getId(), thread.getName()); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public void insertThenThrow() { + assertInAnnotationTransaction(true); + Person person = personOperations.insertById(Person.class).inScope(scopeName).inCollection(collectionName).one(new Person("Walter", "White")); + SimulateFailureException.throwEx(); + } + + @Autowired CouchbaseCallbackTransactionManager callbackTm; + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional + public Person replacePerson(Person person, AtomicInteger tryCount) { + tryCount.incrementAndGet(); + // Note that passing in a Person and replace it in this way, is not supported + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + } + + @Transactional + public void replaceEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.replaceById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(fetched); + } + + @Transactional + public void removeEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.removeById(PersonWithoutVersion.class).inScope(scopeName).inCollection(collectionName).oneEntity(fetched); + } + + @Transactional + public Person declarativeFindReplaceTwicePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(person.id()); + Person pUpdated = personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(p); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(pUpdated); + } + + @Transactional(timeout = 2) + public Person replace(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + System.err.println("try: " + tryCount.get()); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(person); + } + + @Transactional + public Person fetchAndReplace(String id, AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id); + Person modified = callback.apply(p); + return personOperations.replaceById(Person.class).inScope(scopeName).inCollection(collectionName).one(modified); + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + + @Transactional + public void fetchAndRemove(String id, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).inScope(scopeName).inCollection(collectionName).one(id); + personOperations.removeById(Person.class).inScope(scopeName).inCollection(collectionName).oneEntity(p); + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java index 89219517c..8be0c15c5 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -22,7 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInReactiveTransaction; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; import java.util.ArrayList; import java.util.List; @@ -53,8 +55,8 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import com.couchbase.client.core.error.transaction.AttemptExpiredException; @@ -66,7 +68,8 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig( - classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateIntegrationTests.PersonService.class }) + classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateIntegrationTests.PersonService.class }) +@DirtiesContext public class CouchbaseTransactionalTemplateIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -393,6 +396,7 @@ public Person declarativeSavePerson(Person person) { @Transactional public Person declarativeSavePersonWithThread(Person person, Thread thread) { assertInAnnotationTransaction(true); + assertInReactiveTransaction(); long currentThreadId = Thread.currentThread().getId(); System.out.printf("Thread %d %s, started from %d %s%n", Thread.currentThread().getId(), Thread.currentThread().getName(), thread.getId(), thread.getName()); @@ -482,19 +486,4 @@ public void fetchAndRemove(String id, AtomicInteger tryCount) { } - static void assertInAnnotationTransaction(boolean inTransaction) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor")) { - if (inTransaction) { - return; - } - } - } - if (!inTransaction) { - return; - } - throw new RuntimeException( - "in transaction = " + (!inTransaction) + " but expected in annotation transaction = " + inTransaction); - } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java index d8c82e52f..5eb3fafb4 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -39,6 +39,7 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Service; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.Transactional; @@ -59,8 +60,8 @@ * @author Tigran Babloyan */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalUnsettableParametersIntegrationTests.PersonService.class }) +@SpringJUnitConfig(classes = { TransactionsConfig.class, CouchbaseTransactionalUnsettableParametersIntegrationTests.PersonService.class }) +@DirtiesContext public class CouchbaseTransactionalUnsettableParametersIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -265,4 +266,5 @@ public T doInTransaction(AtomicInteger tryCount, Function savePersonErrors(Person person) { . flatMap(it -> Mono.error(new SimulateFailureException())); } + @Transactional + public Person savePersonBlocking(Person person) { + System.err.println("savePerson: "+Thread.currentThread().getName() +" "+ System.identityHashCode(Util.getSecurityContext())); + return personOperations.insertById(Person.class).one(person); + } + @Transactional public Mono savePerson(Person person) { + System.err.println("savePerson: "+Thread.currentThread().getName() +" "+ System.identityHashCode(Util.getSecurityContext())); return TransactionalSupport.checkForTransactionInThreadLocalStorage().map(stat -> { assertTrue(stat.isPresent(), "Not in transaction"); System.err.println("In a transaction!!"); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java index cd4574b38..bbd19cb22 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -48,7 +49,6 @@ import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; /** @@ -58,7 +58,8 @@ */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig( - classes = { TransactionsConfig.class, ReactiveTransactionalTemplateIntegrationTests.PersonService.class }) + classes = { TransactionsConfig.class, ReactiveTransactionalTemplateIntegrationTests.PersonService.class }) +@DirtiesContext public class ReactiveTransactionalTemplateIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired PersonService personService; @@ -200,4 +201,6 @@ public Flux doInTransactionReturningFlux(AtomicInteger tryCount, }); } } + + static public class TransactionsConfig extends org.springframework.data.couchbase.transactions.TransactionsConfig {} } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java index 363ae85e3..ddd835c4b 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java index 9a75de77a..1a073de78 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java index 826e7c6fd..a94a90e97 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -49,6 +49,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; @@ -63,6 +64,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) +@DirtiesContext public class TransactionTemplateIntegrationTests extends JavaIntegrationTests { @Autowired TransactionTemplate transactionTemplate; @Autowired CouchbaseCallbackTransactionManager transactionManager; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java index 6c1f61686..c571188b9 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java index ba88d765e..978150c9d 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -18,6 +18,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Mono; import java.util.concurrent.atomic.AtomicInteger; @@ -31,7 +33,6 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.transactions.TransactionsConfig; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -51,6 +52,7 @@ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(classes = { TransactionsConfig.class, SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.PersonService.class }) +@DirtiesContext public class SDKReactiveTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -129,4 +131,5 @@ public Mono doInService(AtomicInteger tryCount, Function T doInService(AtomicInteger tryCount, Function { PersonWithoutVersion p = new PersonWithoutVersion("Walter", "White"); - return reactiveOps.save(p); + return reactiveOps.save(p).then(assertInReactiveTransaction()); }).block(); } @@ -75,18 +79,22 @@ public void reactiveSaveInReactiveTransaction() { @Test public void reactiveSaveInBlockingTransaction() { couchbaseClientFactory.getCluster().transactions().run(ctx -> { + assertInTransaction(); PersonWithoutVersion p = new PersonWithoutVersion("Walter", "White"); reactiveOps.save(p).block(); }); } + // This should not work because ops.save(p) calls block() so everything in that call + // does not have the reactive context (which has the transaction context) + // what happens is the ops.save(p) is not in a transaction. (it will call upsert instead of insert) @DisplayName("ReactiveCouchbaseTemplate.save() called inside a reactive SDK transaction should work") @Test public void blockingSaveInReactiveTransaction() { couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { PersonWithoutVersion p = new PersonWithoutVersion("Walter", "White"); ops.save(p); - return Mono.empty(); + return assertInReactiveTransaction(); }).block(); } @@ -94,6 +102,7 @@ public void blockingSaveInReactiveTransaction() { @Test public void blockingSaveInBlockingTransaction() { couchbaseClientFactory.getCluster().transactions().run(ctx -> { + assertInTransaction(); PersonWithoutVersion p = new PersonWithoutVersion("Walter", "White"); ops.save(p); }); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java index 696af25a3..59e87b860 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -24,6 +24,8 @@ import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import org.springframework.test.annotation.DirtiesContext; import reactor.util.annotation.Nullable; import java.time.Duration; @@ -46,7 +48,6 @@ import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; import org.springframework.data.couchbase.transactions.ReplaceLoopThread; import org.springframework.data.couchbase.transactions.SimulateFailureException; -import org.springframework.data.couchbase.transactions.TransactionsConfig; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -69,6 +70,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) +@DirtiesContext public class SDKTransactionsTemplateIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired CouchbaseClientFactory couchbaseClientFactory; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java index 176340860..c00f83080 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors + * Copyright 2022-2025 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. @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.springframework.data.couchbase.core.TransactionalSupport; +import reactor.core.publisher.Mono; /** * Utility methods for transaction tests. @@ -35,4 +36,16 @@ public static void assertInTransaction() { public static void assertNotInTransaction() { assertFalse(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); } + + public static Mono assertInReactiveTransaction(T... obj) { + return Mono.deferContextual((ctx1) -> + TransactionalSupport.checkForTransactionInThreadLocalStorage() + .flatMap(ctx2 -> ctx2.isPresent() ? (obj.length>0 ? Mono.just(obj[0]) : Mono.empty()) : Mono.error(new RuntimeException("in transaction")))); + } + + public static Mono assertNotInReactiveTransaction(T... obj) { + return Mono.deferContextual((ctx1) -> + TransactionalSupport.checkForTransactionInThreadLocalStorage() + .flatMap(ctx2 -> !ctx2.isPresent() ? (obj.length>0 ? Mono.just(obj[0]) : Mono.empty()) : Mono.error(new RuntimeException("in transaction")))); + } } diff --git a/src/test/java/org/springframework/data/couchbase/util/Capabilities.java b/src/test/java/org/springframework/data/couchbase/util/Capabilities.java index 1bec97a19..83932170a 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Capabilities.java +++ b/src/test/java/org/springframework/data/couchbase/util/Capabilities.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 0921e8803..bf61b7867 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.time.Duration; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -34,6 +35,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; @@ -43,6 +45,7 @@ import com.couchbase.client.core.env.SecurityConfig; import com.couchbase.client.core.env.SeedNode; import com.couchbase.client.core.error.IndexFailureException; +import com.couchbase.client.java.Cluster; import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions; import com.couchbase.client.java.manager.query.CreateQueryIndexOptions; @@ -61,6 +64,8 @@ public abstract class ClusterAwareIntegrationTests { private static TestClusterConfig testClusterConfig; public static final Logger LOGGER = LoggerFactory.getLogger(ClusterAwareIntegrationTests.class); + @Autowired Cluster cluster; // so we can save it to clusterToDisconnect in @BeforeEach + static public Cluster clusterToDisconnect; // so we can disconnect it in @AfterAll @BeforeAll static void setup(TestClusterConfig config) { @@ -70,8 +75,10 @@ static void setup(TestClusterConfig config) { .transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false))) .build(); String connectString = connectionString(); + Cluster tmpCluster = null; try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, authenticator(), bucketName(), null, env)) { + tmpCluster = couchbaseClientFactory.getCluster(); couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions .createPrimaryQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // this is for the N1qlJoin test @@ -80,8 +87,10 @@ static void setup(TestClusterConfig config) { couchbaseClientFactory.getCluster().queryIndexes().createIndex(bucketName(), "parent_idx", fieldList, CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // .with("_class", "org.springframework.data.couchbase.domain.Address")); + logCluster(couchbaseClientFactory.getCluster(), "$"); } catch (IndexFailureException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife); + logDisconnect(tmpCluster, "IndexFailureException"); } catch (IOException ioe) { throw new RuntimeException(ioe); } @@ -160,17 +169,66 @@ protected static Set seedNodes() { .collect(Collectors.toSet()); } + static Set flagged = new HashSet<>(); + + @AfterEach + public void afterEach() { + if (clusterToDisconnect == null && !flagged.contains(this.getClass().getSimpleName())) { + flagged.add(this.getClass().getSimpleName()); + logMessage("CoreDisconnected:\"coreId\":\"" + this.getClass().getSimpleName() + "\""); + } + } + + @AfterAll + public static void afterAll() { + //if (clusterToDisconnect != null) { + // logDisconnect(clusterToDisconnect, "$"); + // clusterToDisconnect = null; + //} else { + // already flaged by afterEach + //} + logMessage("CoreDisconnected:\"coreId\":\"------------------\""); + callSuperAfterAll(new Object() {}); + } + + static Set disconnectedMap = new HashSet<>(); + + public static void logCluster(Cluster cluster, String s) { + if (cluster == null || disconnectedMap.contains(cluster.core().context().id())) { + org.slf4j.LoggerFactory.getLogger("com.couchbase.core").info("CoreDisconnectedAutoAlready:\"coreId\":\"" + + String.format("0x%16x", 0) + "auto missed<" + s + ">\""); + } else { + disconnectedMap.add(cluster.core().context().id()); + org.slf4j.LoggerFactory.getLogger("com.couchbase.core").info("CoreDisconnectedAuto:\"coreId\":\"" + + String.format("0x%x", cluster.core().context().id()) + "(" + s + ")\""); + //cluster.environment().shutdown(Duration.ofSeconds(60)); needs to happend after auto disconnect() + } + } + + public static void logDisconnect(Cluster cluster, String s) { + if (cluster == null || disconnectedMap.contains(cluster.core().context().id())) { + org.slf4j.LoggerFactory.getLogger("com.couchbase.core").info( + "CoreDisconnectedAlready:\"coreId\":\"" + String.format("0x%16x", 0) + " missed{" + s + "}\""); + } else { + disconnectedMap.add(cluster.core().context().id()); + org.slf4j.LoggerFactory.getLogger("com.couchbase.core").info("CoreDisconnected:\"coreId\":\"" + + String.format("0x%x", cluster.core().context().id()) + "[" + s + "]\""); + cluster.disconnect(); + cluster.environment().shutdown(Duration.ofSeconds(60)); + } + } + + public static void logMessage(String message) { + org.slf4j.LoggerFactory.getLogger("com.couchbase.core").info(message); + } + @BeforeAll() public static void beforeAll() {} - @AfterAll - public static void afterAll() {} - @BeforeEach - public void beforeEach() {} - - @AfterEach - public void afterEach() {} + public void beforeEach() { + clusterToDisconnect = cluster; + } /** * This should probably be the first call in the @BeforeAll method of a test class. This will call super @BeforeAll diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterInvocationProvider.java b/src/test/java/org/springframework/data/couchbase/util/ClusterInvocationProvider.java index 13d5c0243..a49db2e48 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterInvocationProvider.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterInvocationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterType.java b/src/test/java/org/springframework/data/couchbase/util/ClusterType.java index b9a9ddfba..da3c0be6e 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterType.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterType.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareDefaultScopeIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareDefaultScopeIntegrationTests.java new file mode 100644 index 000000000..a8347738d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareDefaultScopeIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021-2025 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.couchbase.util; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.data.couchbase.domain.Config; + +import com.couchbase.client.core.error.IndexExistsException; +import com.couchbase.client.core.service.ServiceType; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.manager.collection.CollectionManager; + +/** + * Provides Collection support for integration tests + * + * @Author Michael Reiche + */ +public class CollectionAwareDefaultScopeIntegrationTests extends JavaIntegrationTests { + + public static String scopeName = "_default";// + randomString(); + public static String collectionName = "my_collection";// + randomString(); + + public static final String otherScope = "other_scope"; + public static String collectionName2 = "my_collection2";// + randomString(); + public static final String otherCollection = "other_collection";// + randomString(); + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() { + }); + Cluster cluster = Cluster.connect(connectionString(), + ClusterOptions.clusterOptions(authenticator()).environment(environment().build())); + Bucket bucket = cluster.bucket(config().bucketname()); + bucket.waitUntilReady(Duration.ofSeconds(30)); + waitForService(bucket, ServiceType.QUERY); + waitForQueryIndexerToHaveBucket(cluster, config().bucketname()); + CollectionManager collectionManager = bucket.collections(); + + setupScopeCollection(cluster, scopeName, collectionName, collectionManager); + setupScopeCollection(cluster, scopeName, collectionName2, collectionManager); + + if (otherScope != null || otherCollection != null) { + // afterAll should be undoing the creation of scope etc + setupScopeCollection(cluster, otherScope, otherCollection, collectionManager); + } + + try { + // needs an index for this N1ql Join + // create index ix2 on my_bucket(parent_id) where `_class` = 'org.springframework.data.couchbase.domain.Address'; + + List fieldList = new ArrayList<>(); + fieldList.add("parentId"); + cluster.query("CREATE INDEX `parent_idx` ON default:`" + bucketName() + "`." + scopeName + "." + collectionName2 + + "(parentId)"); + } catch (IndexExistsException ife) { + LOGGER.warn("IndexFailureException occurred - ignoring: ", ife.toString()); + } + logDisconnect(cluster, CollectionAwareDefaultScopeIntegrationTests.class.getSimpleName()); + Config.setScopeName(scopeName); + // ApplicationContext ac = new AnnotationConfigApplicationContext(); + // System.out.println(ac); + // the Config class has been modified, these need to be loaded again + // couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + // reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + } + + @AfterAll + public static void afterAll() { + Config.setScopeName(null); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // the Config class has been modified, these need to be loaded again + // couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + // reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + callSuperAfterAll(new Object() { + }); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java index 9879c2b47..351900a75 100644 --- a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors + * Copyright 2021-2025 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. @@ -15,19 +15,12 @@ */ package org.springframework.data.couchbase.util; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; -import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; - import java.time.Duration; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; import com.couchbase.client.core.error.IndexExistsException; @@ -80,21 +73,22 @@ public static void beforeAll() { } catch (IndexExistsException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife.toString()); } - + logDisconnect(cluster, CollectionAwareIntegrationTests.class.getSimpleName()); Config.setScopeName(scopeName); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // System.out.println(ac); // the Config class has been modified, these need to be loaded again - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + // couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + // reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); } @AfterAll public static void afterAll() { Config.setScopeName(null); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); // the Config class has been modified, these need to be loaded again - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + // couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + // reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); callSuperAfterAll(new Object() {}); } } diff --git a/src/test/java/org/springframework/data/couchbase/util/IgnoreWhen.java b/src/test/java/org/springframework/data/couchbase/util/IgnoreWhen.java index 15cbffd59..43911f369 100644 --- a/src/test/java/org/springframework/data/couchbase/util/IgnoreWhen.java +++ b/src/test/java/org/springframework/data/couchbase/util/IgnoreWhen.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index 1ef84ffcc..9c3b3f25c 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -23,8 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; -import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; import static org.springframework.data.couchbase.util.Util.waitUntilCondition; import java.io.IOException; @@ -44,12 +42,8 @@ import org.junit.jupiter.api.function.Executable; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.opentest4j.AssertionFailedError; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.transactions.SimulateFailureException; @@ -85,6 +79,8 @@ import com.couchbase.client.java.search.SearchQuery; import com.couchbase.client.java.search.result.SearchResult; +; + /** * Extends the {@link ClusterAwareIntegrationTests} with java-client specific code. * @@ -94,21 +90,15 @@ @Timeout(value = 10, unit = TimeUnit.MINUTES) // Safety timer so tests can't block CI executors public class JavaIntegrationTests extends ClusterAwareIntegrationTests { - // Autowired annotation is not supported on static fields - static public CouchbaseTemplate couchbaseTemplate; - static public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; - @BeforeAll public static void beforeAll() { Config.setScopeName(null); callSuperBeforeAll(new Object() {}); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), authenticator(), bucketName())) { couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + logCluster(couchbaseClientFactory.getCluster(), "-"); } catch (IOException ioe) { throw new RuntimeException(ioe); } @@ -163,7 +153,7 @@ public static void setupScopeCollection(Cluster cluster, String scopeName, Strin () -> collectionReady(cluster.bucket(config().bucketname()).scope(scopeName).collection(collectionName))); assertNotEquals(scopeSpec, collectionManager.getScope(scopeName)); - assertTrue(collectionManager.getScope(scopeName).collections().contains(collSpec)); + assertTrue(collectionManager.getScope(scopeName).collections().stream().anyMatch( (c) -> c.name().equals(collSpec.name()) && c.scopeName().equals(collSpec.scopeName()))); waitForQueryIndexerToHaveBucket(cluster, collectionName); @@ -239,7 +229,7 @@ protected static void waitForService(final Bucket bucket, final ServiceType serv public static boolean collectionExists(CollectionManager mgr, CollectionSpec spec) { try { ScopeSpec scope = mgr.getScope(spec.scopeName()); - return scope.collections().contains(spec); + return scope.collections().stream().anyMatch( (c) -> c.name().equals(spec.name()) && c.scopeName().equals(spec.scopeName())); } catch (CollectionNotFoundException e) { return false; } @@ -285,7 +275,7 @@ public static CompletableFuture createPrimaryIndex(Cluster cluster, String options.timeout(Duration.ofSeconds(300)); options.ignoreIfExists(true); final CreatePrimaryQueryIndexOptions.Built builtOpts = options.build(); - final String indexName = builtOpts.indexName().orElse(null); + final String indexName = builtOpts.indexName(); String keyspace = "default:`" + bucketName + "`.`" + scopeName + "`.`" + collectionName + "`"; String statement = "CREATE PRIMARY INDEX "; diff --git a/src/test/java/org/springframework/data/couchbase/util/MockTestCluster.java b/src/test/java/org/springframework/data/couchbase/util/MockTestCluster.java index 4d68fce11..6d0af5a6a 100644 --- a/src/test/java/org/springframework/data/couchbase/util/MockTestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/MockTestCluster.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/Services.java b/src/test/java/org/springframework/data/couchbase/util/Services.java index b2c8d6deb..fed8c751e 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Services.java +++ b/src/test/java/org/springframework/data/couchbase/util/Services.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index daea0cb0a..fd3aec7b9 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java index b78884f1d..9ed01e6f9 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/TestNodeConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestNodeConfig.java index 3b271f802..d7a69c719 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestNodeConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestNodeConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java index ea1686fc3..8591fd42b 100644 --- a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors + * Copyright 2012-2025 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. @@ -17,6 +17,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.couchbase.client.core.util.ConnectionString; import okhttp3.Credentials; import okhttp3.FormBody; import okhttp3.OkHttpClient; @@ -53,6 +54,7 @@ public class UnmanagedTestCluster extends TestCluster { private final String hostname; private volatile String bucketname; private long startTime = System.currentTimeMillis(); + private boolean usingCloud; UnmanagedTestCluster(final Properties properties) { String seed = properties.getProperty("cluster.unmanaged.seed"); @@ -61,7 +63,7 @@ public class UnmanagedTestCluster extends TestCluster { seedPort = 18091; EventBus eventBus = new SimpleEventBus(false); eventBus.subscribe(event -> System.err.println("Event: " + event)); - Collection seedNodes = ConnectionStringUtil.seedNodesFromConnectionString("couchbases://" + seed, true, + Collection seedNodes = ConnectionStringUtil.seedNodesFromConnectionString(ConnectionString.create("couchbases://" + seed), true, true, eventBus); hostname = seedNodes.stream().filter((node) -> node.kvPort() != null).findFirst().get().address().toString(); seedHost = "couchbases://" + seed; @@ -73,6 +75,7 @@ public class UnmanagedTestCluster extends TestCluster { } adminUsername = properties.getProperty("cluster.adminUsername"); adminPassword = properties.getProperty("cluster.adminPassword"); + bucketname = properties.getProperty("cluster.unmanaged.bucket"); numReplicas = Integer.parseInt(properties.getProperty("cluster.unmanaged.numReplicas")); HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder().addPlatformTrustedCertificates() @@ -92,8 +95,11 @@ ClusterType type() { TestClusterConfig _start() throws Exception { // no means to create a bucket on Capella // have not created config() yet. - boolean usingCloud = seedHost.endsWith("cloud.couchbase.com"); - bucketname = usingCloud ? "my_bucket" : UUID.randomUUID().toString(); + usingCloud = seedHost.endsWith("cloud.couchbase.com"); + if (usingCloud && bucketname == null) { + throw new RuntimeException("cloud must use an existing bucket. Must specify cluster.unmanaged.bucket"); + } + bucketname = bucketname != null ? bucketname : UUID.randomUUID().toString(); if (!usingCloud) { Response postResponse = httpClient .newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword)) @@ -176,7 +182,7 @@ private void waitUntilAllNodesHealthy() throws Exception { @Override public void close() { try { - if (!bucketname.equals("my_bucket")) { + if (!bucketname.equals("my_bucket") && !usingCloud) { httpClient .newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword)) .url(protocol + "://" + hostname + ":" + seedPort + "/pools/default/buckets/" + bucketname).delete() diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 518c51b3c..cb1f28cc8 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors + * Copyright 2020-2025 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. @@ -20,6 +20,7 @@ import static org.awaitility.Awaitility.with; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.Arrays; import java.util.LinkedList; @@ -155,7 +156,8 @@ public static Pair, List> comprisesNot(Iterable source, T[] al public static void assertInAnnotationTransaction(boolean inTransaction) { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") + if (ste.getClassName() + .startsWith("org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager") || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { if (inTransaction) { return; @@ -169,4 +171,26 @@ public static void assertInAnnotationTransaction(boolean inTransaction) { + " but expected in-annotation-transaction = " + inTransaction); } + static public Object getSecurityContext(){ + Object sc = null; + try { + Class securityContextHolderClass = Class + .forName("org.springframework.security.core.context.SecurityContextHolder"); + sc = securityContextHolderClass.getMethod("getContext").invoke(null); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException cnfe) {} + System.err.println(Thread.currentThread().getName() +" Util.get "+ System.identityHashCode(sc)); + return sc; + } + + static public void setSecurityContext(Object sc) { + System.err.println(Thread.currentThread().getName() +" Util.set "+ System.identityHashCode(sc)); + try { + Class securityContextHolderClass = Class + .forName("org.springframework.security.core.context.SecurityContextHolder"); + sc = securityContextHolderClass.getMethod("setContext", new Class[]{securityContextHolderClass}).invoke(sc); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException cnfe) {} + } + }