diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 606226523e..a5f764579a 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -13,35 +13,28 @@ on: jobs: Inbox: runs-on: ubuntu-latest - if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null + if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null && !contains(join(github.event.issue.labels.*.name, ', '), 'dependency-upgrade') && !contains(github.event.issue.title, 'Release ') steps: - name: Create or Update Issue Card - uses: peter-evans/create-or-update-project-card@v1.1.2 + uses: actions/add-to-project@v1.0.2 with: - project-name: 'Spring Data' - column-name: 'Inbox' - project-location: 'spring-projects' - token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} + project-url: https://github.com/orgs/spring-projects/projects/25 + github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} Pull-Request: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request != null steps: - name: Create or Update Pull Request Card - uses: peter-evans/create-or-update-project-card@v1.1.2 + uses: actions/add-to-project@v1.0.2 with: - project-name: 'Spring Data' - column-name: 'Review pending' - project-location: 'spring-projects' - issue-number: ${{ github.event.pull_request.number }} - token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} + project-url: https://github.com/orgs/spring-projects/projects/25 + github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} Feedback-Provided: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.actor != 'spring-projects-issues' && github.event.pull_request == null && github.event.issue.state == 'open' && contains(toJSON(github.event.issue.labels), 'waiting-for-feedback') steps: - name: Update Project Card - uses: peter-evans/create-or-update-project-card@v1.1.2 + uses: actions/add-to-project@v1.0.2 with: - project-name: 'Spring Data' - column-name: 'Feedback provided' - project-location: 'spring-projects' - token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} + project-url: https://github.com/orgs/spring-projects/projects/25 + github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }} diff --git a/.gitignore b/.gitignore index 419a1317bd..62d57535ac 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,6 @@ work build/ node_modules node -package.json package-lock.json -.mvn/.gradle-enterprise +package.json +.mvn/.develocity diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index ebd7610255..e0857eaa25 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,13 +1,8 @@ - com.gradle - gradle-enterprise-maven-extension - 1.19.2 - - - com.gradle - common-custom-user-data-maven-extension - 1.12.4 + io.spring.develocity.conventions + develocity-conventions-maven-extension + 0.0.22 diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml deleted file mode 100644 index c244063147..0000000000 --- a/.mvn/gradle-enterprise.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - https://ge.spring.io - - - false - true - true - - #{{'0.0.0.0'}} - - - - - true - - - - - ${env.DEVELOCITY_CACHE_USERNAME} - ${env.DEVELOCITY_CACHE_PASSWORD} - - - true - #{env['DEVELOCITY_CACHE_USERNAME'] != null and env['DEVELOCITY_CACHE_PASSWORD'] != null} - - - diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..e27f6e8f5e --- /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 113cc85066..f15f39b88f 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -#Thu Dec 14 08:40:45 CET 2023 -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +#Thu Nov 07 09:47:27 CET 2024 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index 4ce7abdfee..a4820853f1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,7 +33,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-redis-6.2:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg REDIS=${p['docker.redis.6.version']} -f ci/openjdk17-redis-6.2/Dockerfile .") + def image = docker.build("springci/spring-data-with-redis-6.2:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg VERSION=${p['docker.redis.6.version']} -f ci/openjdk17-redis-6.2/Dockerfile .") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -53,7 +53,27 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-redis-7.2:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg REDIS=${p['docker.redis.7.version']} -f ci/openjdk17-redis-7.2/Dockerfile .") + def image = docker.build("springci/spring-data-with-redis-7.2:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg VERSION=${p['docker.redis.7.version']} -f ci/openjdk17-redis-7.2/Dockerfile .") + docker.withRegistry(p['docker.registry'], p['docker.credentials']) { + image.push() + } + } + } + } + stage('Publish JDK 17 + Valkey 8.0 Docker Image') { + when { + anyOf { + changeset "ci/openjdk17-valkey-8.0/Dockerfile" + changeset "Makefile" + changeset "ci/pipeline.properties" + } + } + agent { label 'data' } + options { timeout(time: 20, unit: 'MINUTES') } + + steps { + script { + def image = docker.build("springci/spring-data-with-valkey-8.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg VERSION=${p['docker.valkey.8.version']} -f ci/openjdk17-valkey-8.0/Dockerfile .") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -73,7 +93,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-redis-6.2:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg REDIS=${p['docker.redis.6.version']} -f ci/openjdk21-redis-6.2/Dockerfile .") + def image = docker.build("springci/spring-data-with-redis-6.2:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg VERSION=${p['docker.redis.6.version']} -f ci/openjdk21-redis-6.2/Dockerfile .") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -97,13 +117,14 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-redis-6.2:${p['java.main.tag']}").inside('-v $HOME:/tmp/jenkins-home') { - sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image("springci/spring-data-with-redis-6.2:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + } } } } @@ -125,13 +146,14 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-redis-6.2:${p['java.main.tag']}").inside('-v $HOME:/tmp/jenkins-home') { - sh "PROFILE=runtimehints LONG_TESTS=false JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image("springci/spring-data-with-redis-6.2:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + sh "PROFILE=runtimehints LONG_TESTS=false JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + } } } } @@ -143,13 +165,14 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-redis-6.2:${p['java.next.tag']}").inside('-v $HOME:/tmp/jenkins-home') { - sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image("springci/spring-data-with-redis-6.2:${p['java.next.tag']}").inside(p['docker.java.inside.docker']) { + sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + } } } } @@ -161,13 +184,34 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-redis-7.2:${p['java.main.tag']}").inside('-v $HOME:/tmp/jenkins-home') { - sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image("springci/spring-data-with-redis-7.2:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + } + } + } + } + } + + stage("test: Valkey 8") { + 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("springci/spring-data-with-valkey-8.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + sh "PROFILE=none LONG_TESTS=true JENKINS_USER_NAME=${p['jenkins.user.name']} ci/test.sh" + } } } } @@ -190,25 +234,25 @@ pipeline { environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { - sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + - "DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " + - "DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " + - "GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " + - "./mvnw -s settings.xml -Pci,artifactory " + - "-Dartifactory.server=${p['artifactory.url']} " + - "-Dartifactory.username=${ARTIFACTORY_USR} " + - "-Dartifactory.password=${ARTIFACTORY_PSW} " + - "-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " + - "-Dartifactory.build-name=spring-data-redis " + - "-Dartifactory.build-number=spring-data-redis-${BRANCH_NAME}-build-${BUILD_NUMBER} " + - "-Dmaven.test.skip=true clean deploy -U -B" + 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=${p['artifactory.repository.snapshot']} " + + "-Dartifactory.build-name=spring-data-redis " + + "-Dartifactory.build-number=spring-data-redis-${BRANCH_NAME}-build-${BUILD_NUMBER} " + + "-Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-redis " + + "-Dmaven.test.skip=true clean deploy -U -B" + } } } } @@ -218,10 +262,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/Makefile b/Makefile index 35334f506d..1f6dee240f 100644 --- a/Makefile +++ b/Makefile @@ -12,16 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -REDIS_VERSION:=7.2.4 +VERSION?=7.4.0 +PROJECT?=redis +GH_ORG?=redis SPRING_PROFILE?=ci SHELL=/bin/bash -euo pipefail ####### # Redis ####### -.PRECIOUS: work/redis-%.conf +.PRECIOUS: work/$(PROJECT)-%.conf -work/redis-%.conf: +work/$(PROJECT)-%.conf: @mkdir -p $(@D) echo port $* >> $@ @@ -29,15 +31,15 @@ work/redis-%.conf: echo protected-mode no >> $@ echo bind 0.0.0.0 >> $@ echo notify-keyspace-events Ex >> $@ - echo pidfile $(shell pwd)/work/redis-$*.pid >> $@ - echo logfile $(shell pwd)/work/redis-$*.log >> $@ - echo unixsocket $(shell pwd)/work/redis-$*.sock >> $@ + echo pidfile $(shell pwd)/work/$(PROJECT)-$*.pid >> $@ + echo logfile $(shell pwd)/work/$(PROJECT)-$*.log >> $@ + echo unixsocket $(shell pwd)/work/$(PROJECT)-$*.sock >> $@ echo unixsocketperm 755 >> $@ echo save \"\" >> $@ echo slaveof 127.0.0.1 6379 >> $@ # Handled separately because it's a node with authentication. User: spring, password: data. Default password: foobared -work/redis-6382.conf: +work/$(PROJECT)-6382.conf: @mkdir -p $(@D) echo port 6382 >> $@ @@ -45,9 +47,9 @@ work/redis-6382.conf: echo protected-mode no >> $@ echo bind 0.0.0.0 >> $@ echo notify-keyspace-events Ex >> $@ - echo pidfile $(shell pwd)/work/redis-6382.pid >> $@ - echo logfile $(shell pwd)/work/redis-6382.log >> $@ - echo unixsocket $(shell pwd)/work/redis-6382.sock >> $@ + echo pidfile $(shell pwd)/work/$(PROJECT)-6382.pid >> $@ + echo logfile $(shell pwd)/work/$(PROJECT)-6382.log >> $@ + echo unixsocket $(shell pwd)/work/$(PROJECT)-6382.sock >> $@ echo unixsocketperm 755 >> $@ echo "requirepass foobared" >> $@ echo "user default on #1b58ee375b42e41f0e48ef2ff27d10a5b1f6924a9acdcdba7cae868e7adce6bf ~* +@all" >> $@ @@ -55,7 +57,7 @@ work/redis-6382.conf: echo save \"\" >> $@ # Handled separately because it's the master and all others are slaves -work/redis-6379.conf: +work/$(PROJECT)-6379.conf: @mkdir -p $(@D) echo port 6379 >> $@ @@ -63,18 +65,18 @@ work/redis-6379.conf: echo protected-mode no >> $@ echo bind 0.0.0.0 >> $@ echo notify-keyspace-events Ex >> $@ - echo pidfile $(shell pwd)/work/redis-6379.pid >> $@ - echo logfile $(shell pwd)/work/redis-6379.log >> $@ - echo unixsocket $(shell pwd)/work/redis-6379.sock >> $@ + echo pidfile $(shell pwd)/work/$(PROJECT)-6379.pid >> $@ + echo logfile $(shell pwd)/work/$(PROJECT)-6379.log >> $@ + echo unixsocket $(shell pwd)/work/$(PROJECT)-6379.sock >> $@ echo unixsocketperm 755 >> $@ echo save \"\" >> $@ -work/redis-%.pid: work/redis-%.conf work/redis/bin/redis-server - work/redis/bin/redis-server $< +work/$(PROJECT)-%.pid: work/$(PROJECT)-%.conf work/$(PROJECT)/bin/$(PROJECT)-server + work/$(PROJECT)/bin/$(PROJECT)-server $< -redis-start: work/redis-6379.pid work/redis-6380.pid work/redis-6381.pid work/redis-6382.pid +server-start: work/$(PROJECT)-6379.pid work/$(PROJECT)-6380.pid work/$(PROJECT)-6381.pid work/$(PROJECT)-6382.pid -redis-stop: stop-6379 stop-6380 stop-6381 stop-6382 +server-stop: stop-6379 stop-6380 stop-6381 stop-6382 ########## # Sentinel @@ -110,8 +112,8 @@ work/sentinel-26382.conf: echo sentinel monitor mymaster 127.0.0.1 6382 2 >> $@ echo sentinel auth-pass mymaster foobared >> $@ -work/sentinel-%.pid: work/sentinel-%.conf work/redis-6379.pid work/redis/bin/redis-server - work/redis/bin/redis-server $< --sentinel +work/sentinel-%.pid: work/sentinel-%.conf work/$(PROJECT)-6379.pid work/$(PROJECT)/bin/$(PROJECT)-server + work/$(PROJECT)/bin/$(PROJECT)-server $< --sentinel sentinel-start: work/sentinel-26379.pid work/sentinel-26380.pid work/sentinel-26381.pid work/sentinel-26382.pid @@ -136,20 +138,20 @@ work/cluster-%.conf: echo logfile $(shell pwd)/work/cluster-$*.log >> $@ echo save \"\" >> $@ -work/cluster-%.pid: work/cluster-%.conf work/redis/bin/redis-server - work/redis/bin/redis-server $< & +work/cluster-%.pid: work/cluster-%.conf work/$(PROJECT)/bin/$(PROJECT)-server + work/$(PROJECT)/bin/$(PROJECT)-server $< & cluster-start: work/cluster-7379.pid work/cluster-7380.pid work/cluster-7381.pid work/cluster-7382.pid sleep 1 work/meet-%: - -work/redis/bin/redis-cli -p $* cluster meet 127.0.0.1 7379 + -work/$(PROJECT)/bin/$(PROJECT)-cli -p $* cluster meet 127.0.0.1 7379 # Handled separately because this node is a replica work/meet-7382: - -work/redis/bin/redis-cli -p 7382 cluster meet 127.0.0.1 7379 + -work/$(PROJECT)/bin/$(PROJECT)-cli -p 7382 cluster meet 127.0.0.1 7379 sleep 2 - -work/redis/bin/redis-cli -p 7382 cluster replicate $(shell work/redis/bin/redis-cli -p 7379 cluster myid) + -work/$(PROJECT)/bin/$(PROJECT)-cli -p 7382 cluster replicate $(shell work/$(PROJECT)/bin/$(PROJECT)-cli -p 7379 cluster myid) cluster-meet: work/meet-7380 work/meet-7381 work/meet-7382 sleep 1 @@ -157,9 +159,9 @@ cluster-meet: work/meet-7380 work/meet-7381 work/meet-7382 cluster-stop: stop-7379 stop-7380 stop-7381 stop-7382 cluster-slots: - -work/redis/bin/redis-cli -p 7379 cluster addslots $(shell seq 0 5460) - -work/redis/bin/redis-cli -p 7380 cluster addslots $(shell seq 5461 10922) - -work/redis/bin/redis-cli -p 7381 cluster addslots $(shell seq 10923 16383) + -work/$(PROJECT)/bin/$(PROJECT)-cli -p 7379 cluster addslots $(shell seq 0 5460) + -work/$(PROJECT)/bin/$(PROJECT)-cli -p 7380 cluster addslots $(shell seq 5461 10922) + -work/$(PROJECT)/bin/$(PROJECT)-cli -p 7381 cluster addslots $(shell seq 10923 16383) cluster-init: cluster-start cluster-meet cluster-slots @@ -172,38 +174,38 @@ clean: clobber: rm -rf work -work/redis/bin/redis-cli work/redis/bin/redis-server: - @mkdir -p work/redis +work/$(PROJECT)/bin/$(PROJECT)-cli work/$(PROJECT)/bin/$(PROJECT)-server: + @mkdir -p work/$(PROJECT) - curl -sSL https://github.com/redis/redis/archive/$(REDIS_VERSION).tar.gz | tar xzf - -C work - $(MAKE) -C work/redis-$(REDIS_VERSION) -j - $(MAKE) -C work/redis-$(REDIS_VERSION) PREFIX=$(shell pwd)/work/redis install - rm -rf work/redis-$(REDIS_VERSION) + curl -sSL https://github.com/$(GH_ORG)/$(PROJECT)/archive/refs/tags/$(VERSION).tar.gz | tar xzf - -C work + $(MAKE) -C work/$(PROJECT)-$(VERSION) -j + $(MAKE) -C work/$(PROJECT)-$(VERSION) PREFIX=$(shell pwd)/work/$(PROJECT) install + rm -rf work/$(PROJECT)-$(VERSION) -start: redis-start sentinel-start cluster-init +start: server-start sentinel-start cluster-init -stop-%: work/redis/bin/redis-cli - -work/redis/bin/redis-cli -p $* shutdown +stop-%: work/$(PROJECT)/bin/$(PROJECT)-cli + -work/$(PROJECT)/bin/$(PROJECT)-cli -p $* shutdown -stop-6382: work/redis/bin/redis-cli - -work/redis/bin/redis-cli -a foobared -p 6382 shutdown +stop-6382: work/$(PROJECT)/bin/$(PROJECT)-cli + -work/$(PROJECT)/bin/$(PROJECT)-cli -a foobared -p 6382 shutdown -stop-26382: work/redis/bin/redis-cli - -work/redis/bin/redis-cli -a foobared -p 26382 shutdown +stop-26382: work/$(PROJECT)/bin/$(PROJECT)-cli + -work/$(PROJECT)/bin/$(PROJECT)-cli -a foobared -p 26382 shutdown -stop: redis-stop sentinel-stop cluster-stop +stop: server-stop sentinel-stop cluster-stop test: $(MAKE) start sleep 1 - ./mvnw clean test -U -P$(SPRING_PROFILE) -Dredis.server.version=$(REDIS_VERSION) || (echo "maven failed $$?"; exit 1) + ./mvnw clean test -U -P$(SPRING_PROFILE) || (echo "maven failed $$?"; exit 1) $(MAKE) stop $(MAKE) clean all-tests: $(MAKE) start sleep 1 - ./mvnw clean test -U -DrunLongTests=true -P$(SPRING_PROFILE) -Dredis.server.version=$(REDIS_VERSION) || (echo "maven failed $$?"; exit 1) + ./mvnw clean test -U -DrunLongTests=true -P$(SPRING_PROFILE) || (echo "maven failed $$?"; exit 1) $(MAKE) stop $(MAKE) clean diff --git a/README.adoc b/README.adoc index 72089cab87..93d3896580 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,9 @@ -image:https://spring.io/badges/spring-data-redis/ga.svg[Spring Data Redis,link=https://spring.io/projects/spring-data-redis/#quick-start] -image:https://spring.io/badges/spring-data-redis/snapshot.svg[Spring Data Redis,link=https://spring.io/projects/spring-data-redis/#quick-start] - -= Spring Data Redis image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-redis%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-redis/] 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%20Data%20Redis"] += Spring Data Redis image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-redis%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-redis/] 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%20Data%20Redis"] The primary goal of the https://spring.io/projects/spring-data/[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services. -This modules provides integration with the https://redis.io/[Redis] store. +This module provides integration with the https://redis.io/[Redis] store. +It is also tested to work with https://valkey.io/[Valkey] on a best-effort basis as long as Valkey remains largely compatible with Redis. == Features @@ -38,7 +36,7 @@ public class Example { // inject the actual template @Autowired - private RedisTemplate template; + private RedisTemplate redisTemplate; // inject the template as ListOperations // can also inject as Value, Set, ZSet, and HashOperations @@ -102,7 +100,6 @@ https://docs.spring.io/spring-data/redis/reference/[reference documentation], an If you are just starting out with Spring, try one of the https://spring.io/guides[guides]. * If you are upgrading, check out the https://github.com/spring-projects/spring-data-commons/wiki#release-notes[Release notes] for "`new and noteworthy`" features. * Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-data[`spring-data-redis`]. -You can also chat with the community on https://gitter.im/spring-projects/spring-data[Gitter]. * Report bugs with Spring Data Redis at https://github.com/spring-projects/spring-data-redis/issues/new[github.com/spring-projects/spring-data-redis]. == Reporting Issues diff --git a/ci/openjdk17-redis-6.2/Dockerfile b/ci/openjdk17-redis-6.2/Dockerfile index b983e39fb0..8704997bf6 100644 --- a/ci/openjdk17-redis-6.2/Dockerfile +++ b/ci/openjdk17-redis-6.2/Dockerfile @@ -1,16 +1,20 @@ ARG BASE FROM ${BASE} # Any ARG statements before FROM are cleared. -ARG REDIS +ARG VERSION +ENV VERSION=${VERSION} # Copy Spring Data Redis's Makefile into the container COPY ./Makefile / RUN set -eux; \ -# sed -i -e 's/http/https/g' /etc/apt/sources.list ; \ + sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ + sed -i -e 's/http/https/g' /etc/apt/sources.list && \ apt-get update ; \ - apt-get install -y build-essential ; \ - make work/redis/bin/redis-cli work/redis/bin/redis-server REDIS_VERSION=${REDIS}; \ + apt-get install -y build-essential curl; \ + make work/redis/bin/redis-cli work/redis/bin/redis-server VERSION=${VERSION}; \ chmod -R o+rw work; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; diff --git a/ci/openjdk17-redis-7.2/Dockerfile b/ci/openjdk17-redis-7.2/Dockerfile index 6251772188..8704997bf6 100644 --- a/ci/openjdk17-redis-7.2/Dockerfile +++ b/ci/openjdk17-redis-7.2/Dockerfile @@ -1,17 +1,20 @@ ARG BASE FROM ${BASE} # Any ARG statements before FROM are cleared. -ARG REDIS -ENV REDIS_VERSION=${REDIS} +ARG VERSION +ENV VERSION=${VERSION} # Copy Spring Data Redis's Makefile into the container COPY ./Makefile / RUN set -eux; \ -# sed -i -e 's/http/https/g' /etc/apt/sources.list ; \ + sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ + sed -i -e 's/http/https/g' /etc/apt/sources.list && \ apt-get update ; \ - apt-get install -y build-essential ; \ - make work/redis/bin/redis-cli work/redis/bin/redis-server REDIS_VERSION=${REDIS}; \ + apt-get install -y build-essential curl; \ + make work/redis/bin/redis-cli work/redis/bin/redis-server VERSION=${VERSION}; \ chmod -R o+rw work; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; diff --git a/ci/openjdk17-valkey-8.0/Dockerfile b/ci/openjdk17-valkey-8.0/Dockerfile new file mode 100644 index 0000000000..5b040438ea --- /dev/null +++ b/ci/openjdk17-valkey-8.0/Dockerfile @@ -0,0 +1,22 @@ +ARG BASE +FROM ${BASE} +# Any ARG statements before FROM are cleared. +ARG VERSION +ENV VERSION=${VERSION} +ENV PROJECT=valkey +ENV GH_ORG=valkey-io + +# Copy Spring Data Redis's Makefile into the container +COPY ./Makefile / + +RUN set -eux; \ + sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ + sed -i -e 's/http/https/g' /etc/apt/sources.list && \ + apt-get update ; \ + apt-get install -y build-essential curl; \ + make work/valkey/bin/valkey-cli work/valkey/bin/valkey-server VERSION=${VERSION}; \ + chmod -R o+rw work; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/*; diff --git a/ci/openjdk21-redis-6.2/Dockerfile b/ci/openjdk21-redis-6.2/Dockerfile index b983e39fb0..a92d2b8fd6 100644 --- a/ci/openjdk21-redis-6.2/Dockerfile +++ b/ci/openjdk21-redis-6.2/Dockerfile @@ -1,16 +1,20 @@ ARG BASE FROM ${BASE} # Any ARG statements before FROM are cleared. -ARG REDIS +ARG VERSION +ENV VERSION=${VERSION} # Copy Spring Data Redis's Makefile into the container COPY ./Makefile / RUN set -eux; \ -# sed -i -e 's/http/https/g' /etc/apt/sources.list ; \ + sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ + sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ + sed -i -e 's/http/https/g' /etc/apt/sources.list && \ apt-get update ; \ - apt-get install -y build-essential ; \ - make work/redis/bin/redis-cli work/redis/bin/redis-server REDIS_VERSION=${REDIS}; \ + apt-get install -y build-essential curl ; \ + make work/redis/bin/redis-cli work/redis/bin/redis-server VERSION=${VERSION}; \ chmod -R o+rw work; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 6ff93899b4..cde4a8e881 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,24 +1,20 @@ # Java versions -java.main.tag=17.0.9_9-jdk-focal -java.next.tag=21.0.1_12-jdk-jammy +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.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.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.25 -docker.mongodb.5.0.version=5.0.21 -docker.mongodb.6.0.version=6.0.10 -docker.mongodb.7.0.version=7.0.2 +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.13 docker.redis.7.version=7.2.4 -# Supported versions of Cassandra -docker.cassandra.3.version=3.11.16 - # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker -v $HOME:/tmp/jenkins-home @@ -26,9 +22,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.cache.credentials=gradle_enterprise_cache_user 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 06db99ec5d..5bee10b054 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -10,15 +10,10 @@ cwd=$(pwd) # Launch Redis in proper configuration pushd /tmp && ln -s /work && make -f $cwd/Makefile start && popd -export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} -export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} export JENKINS_USER=${JENKINS_USER_NAME} -# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY -export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} - # Execute maven test -MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean test -P${PROFILE} -DrunLongTests=${LONG_TESTS:-false} -Dredis.server.version=${REDIS_VERSION:-unknown} -U -B +MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-redis clean test -P${PROFILE} -DrunLongTests=${LONG_TESTS:-false} -Dredis.server.version=${REDIS_VERSION:-unknown} -U -B # Capture resulting exit code from maven (pass/fail) RESULT=$? diff --git a/pom.xml b/pom.xml index 507b76dd15..b780d1e99c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.3.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data Redis Spring Data module for Redis @@ -14,20 +14,19 @@ org.springframework.data.build spring-data-parent - 3.3.0-SNAPSHOT + 4.0.0-SNAPSHOT - 3.3.0-SNAPSHOT - 3.3.0-SNAPSHOT - 4.0.2 + 4.0.0-SNAPSHOT + 4.0.0-SNAPSHOT 1.9.4 - 1.4.20 + 1.4.21 2.11.1 - 6.3.2.RELEASE - 5.0.2 + 6.6.0.RELEASE + 6.0.0 1.01 - 4.1.107.Final + 4.1.121.Final spring.data.redis @@ -51,6 +50,14 @@ ${pool} + + io.netty + netty-bom + ${netty} + pom + import + + @@ -104,7 +111,6 @@ io.netty netty-transport-native-epoll linux-x86_64 - ${netty} test @@ -112,7 +118,6 @@ io.netty netty-transport-native-kqueue osx-x86_64 - ${netty} test @@ -276,6 +281,13 @@ test + + org.msgpack + jackson-dataformat-msgpack + 0.9.8 + test + + edu.umd.cs.mtc multithreadedtc @@ -317,17 +329,6 @@ maven-assembly-plugin - - org.asciidoctor - asciidoctor-maven-plugin - - - ${lettuce} - ${jedis} - - - - @@ -378,7 +379,7 @@ - io.spring.maven.antora + org.antora antora-maven-plugin diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index b68f34c9b8..b90d6ab326 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -3,8 +3,7 @@ # The purpose of this Antora playbook is to build the docs in the current branch. antora: extensions: - - '@antora/collector-extension' - - require: '@springio/antora-extensions/root-component-extension' + - require: '@springio/antora-extensions' root_component_name: 'data-redis' site: title: Spring Data Redis @@ -22,13 +21,12 @@ content: start_path: src/main/antora asciidoc: attributes: - page-pagination: '' hide-uri-scheme: '@' tabs-sync-option: '@' - chomp: 'all' extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/javadoc-extension' sourcemap: true urls: latest_version_segment: '' @@ -38,5 +36,5 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip + 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 index 3b978752a0..f2cc6bda15 100644 --- a/src/main/antora/antora.yml +++ b/src/main/antora/antora.yml @@ -10,3 +10,8 @@ ext: 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 index 11f202b10c..7cc4db98ce 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -44,4 +44,6 @@ * xref:appendix.adoc[] -* https://github.com/spring-projects/spring-data-commons/wiki[Wiki] + +* 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/antora/modules/ROOT/pages/appendix.adoc b/src/main/antora/modules/ROOT/pages/appendix.adoc index 46feff0611..669bf82204 100644 --- a/src/main/antora/modules/ROOT/pages/appendix.adoc +++ b/src/main/antora/modules/ROOT/pages/appendix.adoc @@ -8,193 +8,201 @@ link:https://www.springframework.org/schema/redis/spring-redis-1.0.xsd[Spring Da [[supported-commands]] == Supported Commands + .Redis commands supported by `RedisTemplate` [width="50%",cols="<2,^1",options="header"] |========================================================= |Command |Template Support -|APPEND |X -|AUTH |X -|BGREWRITEAOF |X -|BGSAVE |X -|BITCOUNT |X -|BITFIELD |X -|BITOP |X -|BLPOP |X -|BRPOP |X -|BRPOPLPUSH |X -|CLIENT KILL |X -|CLIENT GETNAME |X -|CLIENT LIST |X -|CLIENT SETNAME |X -|CLUSTER SLOTS |- -|COMMAND |- -|COMMAND COUNT |- -|COMMAND GETKEYS |- -|COMMAND INFO |- -|CONFIG GET |X -|CONFIG RESETSTAT |X -|CONFIG REWRITE |- -|CONFIG SET |X -|DBSIZE |X -|DEBUG OBJECT |- -|DEBUG SEGFAULT |- -|DECR |X -|DECRBY |X -|DEL |X -|DISCARD |X -|DUMP |X -|ECHO |X -|EVAL |X -|EVALSHA |X -|EXEC |X -|EXISTS |X -|EXPIRE |X -|EXPIREAT |X -|FLUSHALL |X -|FLUSHDB |X -|GEOADD |X -|GEODIST |X -|GEOHASH |X -|GEOPOS |X -|GEORADIUS |X -|GEORADIUSBYMEMBER |X -|GEOSEARCH |X -|GEOSEARCHSTORE |X -|GET |X -|GETBIT |X -|GETRANGE |X -|GETSET |X -|HDEL |X -|HEXISTS |X -|HGET |X -|HGETALL |X -|HINCRBY |X -|HINCRBYFLOAT |X -|HKEYS |X -|HLEN |X -|HMGET |X -|HMSET |X -|HSCAN |X -|HSET |X -|HSETNX |X -|HVALS |X -|INCR |X -|INCRBY |X -|INCRBYFLOAT |X -|INFO |X -|KEYS |X -|LASTSAVE |X -|LINDEX |X -|LINSERT |X -|LLEN |X -|LPOP |X -|LPUSH |X -|LPUSHX |X -|LRANGE |X -|LREM |X -|LSET |X -|LTRIM |X -|MGET |X -|MIGRATE |- -|MONITOR |- -|MOVE |X -|MSET |X -|MSETNX |X -|MULTI |X -|OBJECT |- -|PERSIST |X -|PEXIPRE |X -|PEXPIREAT |X -|PFADD |X -|PFCOUNT |X -|PFMERGE |X -|PING |X -|PSETEX |X -|PSUBSCRIBE |X -|PTTL |X -|PUBLISH |X -|PUBSUB |- -|PUBSUBSCRIBE |- -|QUIT |X -|RANDOMKEY |X -|RENAME |X -|RENAMENX |X -|REPLICAOF |X -|RESTORE |X -|ROLE |- -|RPOP |X -|RPOPLPUSH |X -|RPUSH |X -|RPUSHX |X -|SADD |X -|SAVE |X -|SCAN |X -|SCARD |X -|SCRIPT EXITS |X -|SCRIPT FLUSH |X -|SCRIPT KILL |X -|SCRIPT LOAD |X -|SDIFF |X -|SDIFFSTORE |X -|SELECT |X -|SENTINEL FAILOVER |X +|APPEND |X +|AUTH |X +|BGREWRITEAOF |X +|BGSAVE |X +|BITCOUNT |X +|BITFIELD |X +|BITOP |X +|BLPOP |X +|BRPOP |X +|BRPOPLPUSH |X +|CLIENT KILL |X +|CLIENT GETNAME |X +|CLIENT LIST |X +|CLIENT SETNAME |X +|CLUSTER SLOTS |- +|COMMAND |- +|COMMAND COUNT |- +|COMMAND GETKEYS |- +|COMMAND INFO |- +|CONFIG GET |X +|CONFIG RESETSTAT |X +|CONFIG REWRITE |- +|CONFIG SET |X +|DBSIZE |X +|DEBUG OBJECT |- +|DEBUG SEGFAULT |- +|DECR |X +|DECRBY |X +|DEL |X +|DISCARD |X +|DUMP |X +|ECHO |X +|EVAL |X +|EVALSHA |X +|EXEC |X +|EXISTS |X +|EXPIRE |X +|EXPIREAT |X +|FLUSHALL |X +|FLUSHDB |X +|GEOADD |X +|GEODIST |X +|GEOHASH |X +|GEOPOS |X +|GEORADIUS |X +|GEORADIUSBYMEMBER |X +|GEOSEARCH |X +|GEOSEARCHSTORE |X +|GET |X +|GETBIT |X +|GETRANGE |X +|GETSET |X +|HDEL |X +|HEXISTS |X +|HEXPIRE |X +|HEXPIREAT |X +|HPEXPIRE |X +|HPEXPIREAT |X +|HPERSIST |X +|HTTL |X +|HPTTL |X +|HGET |X +|HGETALL |X +|HINCRBY |X +|HINCRBYFLOAT |X +|HKEYS |X +|HLEN |X +|HMGET |X +|HMSET |X +|HSCAN |X +|HSET |X +|HSETNX |X +|HVALS |X +|INCR |X +|INCRBY |X +|INCRBYFLOAT |X +|INFO |X +|KEYS |X +|LASTSAVE |X +|LINDEX |X +|LINSERT |X +|LLEN |X +|LPOP |X +|LPUSH |X +|LPUSHX |X +|LRANGE |X +|LREM |X +|LSET |X +|LTRIM |X +|MGET |X +|MIGRATE |- +|MONITOR |- +|MOVE |X +|MSET |X +|MSETNX |X +|MULTI |X +|OBJECT |- +|PERSIST |X +|PEXIPRE |X +|PEXPIREAT |X +|PFADD |X +|PFCOUNT |X +|PFMERGE |X +|PING |X +|PSETEX |X +|PSUBSCRIBE |X +|PTTL |X +|PUBLISH |X +|PUBSUB |- +|PUBSUBSCRIBE |- +|QUIT |X +|RANDOMKEY |X +|RENAME |X +|RENAMENX |X +|REPLICAOF |X +|RESTORE |X +|ROLE |- +|RPOP |X +|RPOPLPUSH |X +|RPUSH |X +|RPUSHX |X +|SADD |X +|SAVE |X +|SCAN |X +|SCARD |X +|SCRIPT EXITS |X +|SCRIPT FLUSH |X +|SCRIPT KILL |X +|SCRIPT LOAD |X +|SDIFF |X +|SDIFFSTORE |X +|SELECT |X +|SENTINEL FAILOVER |X |SENTINEL GET-MASTER-ADD-BY-NAME |- -|SENTINEL MASTER | - -|SENTINEL MASTERS |X -|SENTINEL MONITOR |X -|SENTINEL REMOVE |X -|SENTINEL RESET |- -|SENTINEL SET |- -|SENTINEL SLAVES |X -|SET |X -|SETBIT |X -|SETEX |X -|SETNX |X -|SETRANGE |X -|SHUTDOWN |X -|SINTER |X -|SINTERSTORE |X -|SISMEMBER |X -|SLAVEOF |X -|SLOWLOG |- -|SMEMBERS |X -|SMOVE |X -|SORT |X -|SPOP |X -|SRANDMEMBER |X -|SREM |X -|SSCAN |X -|STRLEN |X -|SUBSCRIBE |X -|SUNION |X -|SUNIONSTORE |X -|SYNC |- -|TIME |X -|TTL |X -|TYPE |X -|UNSUBSCRIBE |X -|UNWATCH |X -|WATCH |X -|ZADD |X -|ZCARD |X -|ZCOUNT |X -|ZINCRBY |X -|ZINTERSTORE |X -|ZLEXCOUNT |- -|ZRANGE |X -|ZRANGEBYLEX |- -|ZREVRANGEBYLEX |- -|ZRANGEBYSCORE |X -|ZRANGESTORE |X -|ZRANK |X -|ZREM |X -|ZREMRANGEBYLEX |- -|ZREMRANGEBYRANK |X -|ZREVRANGE |X -|ZREVRANGEBYSCORE |X -|ZREVRANK |X -|ZSCAN |X -|ZSCORE |X -|ZUNINONSTORE |X +|SENTINEL MASTER | - +|SENTINEL MASTERS |X +|SENTINEL MONITOR |X +|SENTINEL REMOVE |X +|SENTINEL RESET |- +|SENTINEL SET |- +|SENTINEL SLAVES |X +|SET |X +|SETBIT |X +|SETEX |X +|SETNX |X +|SETRANGE |X +|SHUTDOWN |X +|SINTER |X +|SINTERSTORE |X +|SISMEMBER |X +|SLAVEOF |X +|SLOWLOG |- +|SMEMBERS |X +|SMOVE |X +|SORT |X +|SPOP |X +|SRANDMEMBER |X +|SREM |X +|SSCAN |X +|STRLEN |X +|SUBSCRIBE |X +|SUNION |X +|SUNIONSTORE |X +|SYNC |- +|TIME |X +|TTL |X +|TYPE |X +|UNSUBSCRIBE |X +|UNWATCH |X +|WATCH |X +|ZADD |X +|ZCARD |X +|ZCOUNT |X +|ZINCRBY |X +|ZINTERSTORE |X +|ZLEXCOUNT |- +|ZRANGE |X +|ZRANGEBYLEX |- +|ZREVRANGEBYLEX |- +|ZRANGEBYSCORE |X +|ZRANGESTORE |X +|ZRANK |X +|ZREM |X +|ZREMRANGEBYLEX |- +|ZREMRANGEBYRANK |X +|ZREVRANGE |X +|ZREVRANGEBYSCORE |X +|ZREVRANK |X +|ZSCAN |X +|ZSCORE |X +|ZUNINONSTORE |X |========================================================= diff --git a/src/main/antora/modules/ROOT/pages/observability.adoc b/src/main/antora/modules/ROOT/pages/observability.adoc index e3a43ae122..a663d514fa 100644 --- a/src/main/antora/modules/ROOT/pages/observability.adoc +++ b/src/main/antora/modules/ROOT/pages/observability.adoc @@ -2,7 +2,7 @@ = Observability Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. -Spring Data Redis ships with a Micrometer integration through the Lettuce driver to collect observations during Redis interaction. +Lettuce ships with a Micrometer integration to collect observations during Redis interaction. Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Redis command. To enable the integration, apply the following configuration to `LettuceClientConfiguration`: @@ -16,7 +16,7 @@ class ObservabilityConfiguration { public ClientResources clientResources(ObservationRegistry observationRegistry) { return ClientResources.builder() - .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) + .tracing(new MicrometerTracing(observationRegistry, "my-redis-cache")) .build(); } @@ -31,77 +31,7 @@ class ObservabilityConfiguration { } ---- -See also https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] for further reference. +See also for further reference: +* https://redis.github.io/lettuce/advanced-usage/#micrometer[Lettuce Tracing] +* https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] . -[[observability-metrics]] -== Observability - Metrics - -Below you can find a list of all metrics declared by this project. - -[[observability-metrics-redis-command-observation]] -== Redis Command Observation - -____ -Timer created around a Redis command execution. -____ - -**Metric name** `spring.data.redis`. **Type** `timer` and **base unit** `seconds`. - -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. - - - -.Low cardinality Keys -[cols="a,a"] -|=== -|Name | Description -|`db.operation`|Redis command value. -|`db.redis.database_index`|Redis database index. -|`db.system`|Database system. -|`db.user`|Redis user. -|`net.peer.name`|Name of the database host. -|`net.peer.port`|Logical remote port number. -|`net.sock.peer.addr`|Mongo peer address. -|`net.sock.peer.port`|Mongo peer port. -|`net.transport`|Network transport. -|=== - -.High cardinality Keys -[cols="a,a"] -|=== -|Name | Description -|`db.statement`|Redis statement. -|`spring.data.redis.command.error`|Redis error response. -|=== - -[[observability-spans]] -== Observability - Spans - -Below you can find a list of all spans declared by this project. - -[[observability-spans-redis-command-observation]] -== Redis Command Observation Span - -> Timer created around a Redis command execution. - -**Span name** `spring.data.redis`. - -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. - - - -.Tag Keys -|=== -|Name | Description -|`db.operation`|Redis command value. -|`db.redis.database_index`|Redis database index. -|`db.statement`|Redis statement. -|`db.system`|Database system. -|`db.user`|Redis user. -|`net.peer.name`|Name of the database host. -|`net.peer.port`|Logical remote port number. -|`net.sock.peer.addr`|Mongo peer address. -|`net.sock.peer.port`|Mongo peer port. -|`net.transport`|Network transport. -|`spring.data.redis.command.error`|Redis error response. -|=== diff --git a/src/main/antora/modules/ROOT/pages/redis/cluster.adoc b/src/main/antora/modules/ROOT/pages/redis/cluster.adoc index 2dda2c0b94..9e1e18f68c 100644 --- a/src/main/antora/modules/ROOT/pages/redis/cluster.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/cluster.adoc @@ -37,14 +37,14 @@ The following example shows a set of commands being run across the cluster: redis-cli@127.0.0.1:7379 > cluster nodes 6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 <1> -7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-20242 <2> -164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-20243 <3> +7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-20252 <2> +164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-20253 <3> b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected <4> ---- [source,java] ---- -RedisClusterConnection connection = connectionFactory.getClusterConnnection(); +RedisClusterConnection connection = connectionFactory.getClusterConnection(); connection.set("thing1", value); <5> connection.set("thing2", value); <6> @@ -88,7 +88,7 @@ redis-cli@127.0.0.1:7379 > cluster nodes [source,java] ---- -RedisClusterConnection connection = connectionFactory.getClusterConnnection(); +RedisClusterConnection connection = connectionFactory.getClusterConnection(); connection.set("thing1", value); // slot: 12182 connection.set("{thing1}.thing2", value); // slot: 12182 @@ -134,5 +134,5 @@ clusterOps.shutdown(NODE_7379); <1> <1> Shut down node at 7379 and cross fingers there is a replica in place that can take over. ==== -NOTE: Redis Cluster pipelining is currently only supported throug the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +NOTE: Redis Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. Same-slot keys are fully supported. diff --git a/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc b/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc index 062eb33d06..2a8f11623f 100644 --- a/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc @@ -9,7 +9,7 @@ Each mode of operation requires specific configuration that is explained in the The easiest way to get started is by using Redis Standalone with a single Redis server, -Configure `LettuceClientConfiguration` or `JedisConnectionFactory`, as shown in the following example: +Configure javadoc:org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory[] or javadoc:org.springframework.data.redis.connection.jedis.JedisConnectionFactory[], as shown in the following example: [source,java] ---- @@ -60,12 +60,12 @@ class WriteToMasterReadFromReplicaConfiguration { } ---- -TIP: For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `RedisStaticMasterReplicaConfiguration` instead of `RedisStandaloneConfiguration`. Please note that `RedisStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. +TIP: For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use javadoc:org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration[] instead of javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[]. Please note that `RedisStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. [[redis:sentinel]] == Redis Sentinel -For dealing with high-availability Redis, Spring Data Redis has support for https://redis.io/topics/sentinel[Redis Sentinel], using `RedisSentinelConfiguration`, as shown in the following example: +For dealing with high-availability Redis, Spring Data Redis has support for https://redis.io/topics/sentinel[Redis Sentinel], using javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], as shown in the following example: [source,java] ---- @@ -113,8 +113,8 @@ Sometimes, direct interaction with one of the Sentinels is required. Using `Redi [[cluster.enable]] == Redis Cluster -xref:redis/cluster.adoc[Cluster support] is based on the same building blocks as non-clustered communication. `RedisClusterConnection`, an extension to `RedisConnection`, handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. -`RedisClusterConnection` instances are created with the `RedisConnectionFactory`, which has to be set up with the associated `RedisClusterConfiguration`, as shown in the following example: +xref:redis/cluster.adoc[Cluster support] is based on the same building blocks as non-clustered communication. javadoc:org.springframework.data.redis.connection.RedisClusterConnection[], an extension to `RedisConnection`, handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. +`RedisClusterConnection` instances are created with the `RedisConnectionFactory`, which has to be set up with the associated javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[], as shown in the following example: .Sample RedisConnectionFactory Configuration for Redis Cluster ==== diff --git a/src/main/antora/modules/ROOT/pages/redis/drivers.adoc b/src/main/antora/modules/ROOT/pages/redis/drivers.adoc index 2f649f3166..52570144d5 100644 --- a/src/main/antora/modules/ROOT/pages/redis/drivers.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/drivers.adoc @@ -154,7 +154,7 @@ public LettuceConnectionFactory lettuceConnectionFactory() { } ---- -For more detailed client configuration tweaks, see https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.html[`LettuceClientConfiguration`]. +For more detailed client configuration tweaks, see javadoc:org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration[]. Lettuce integrates with Netty's https://netty.io/wiki/native-transports.html[native transports], letting you use Unix domain sockets to communicate with Redis. Make sure to include the appropriate native transport dependencies that match your runtime environment. diff --git a/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc b/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc index 4c16e17e98..3a803ad85b 100644 --- a/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc @@ -37,5 +37,5 @@ include::example$examples/ReactiveRedisApplication.java[tags=file] Even in this simple example, there are a few notable things to point out: -* You can create an instance of `RedisTemplate` (or `ReactiveRedisTemplate` for reactive usage) with a `RedisConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. +* You can create an instance of javadoc:org.springframework.data.redis.core.RedisTemplate[] (or javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[]for reactive usage) with a javadoc:org.springframework.data.redis.connection.RedisConnectionFactory[]. Connection factories are an abstraction on top of the supported drivers. * There's no single way to use Redis as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc b/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc index 2ecbc321b7..334a2fd515 100644 --- a/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc @@ -1,21 +1,21 @@ [[redis.hashmappers.root]] = Hash Mapping -Data can be stored by using various data structures within Redis. `Jackson2JsonRedisSerializer` can convert objects in https://en.wikipedia.org/wiki/JSON[JSON] format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): +Data can be stored by using various data structures within Redis. javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] can convert objects in https://en.wikipedia.org/wiki/JSON[JSON] format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): -* Direct mapping, by using `HashOperations` and a xref:redis.adoc#redis:serializer[serializer] +* Direct mapping, by using javadoc:org.springframework.data.redis.core.HashOperations[] and a xref:redis.adoc#redis:serializer[serializer] * Using xref:repositories.adoc[Redis Repositories] -* Using `HashMapper` and `HashOperations` +* Using javadoc:org.springframework.data.redis.hash.HashMapper[] and javadoc:org.springframework.data.redis.core.HashOperations[] [[redis.hashmappers.mappers]] == Hash Mappers -Hash mappers are converters of map objects to a `Map` and back. `HashMapper` is intended for using with Redis Hashes. +Hash mappers are converters of map objects to a `Map` and back. javadoc:org.springframework.data.redis.hash.HashMapper[] is intended for using with Redis Hashes. Multiple implementations are available: -* `BeanUtilsHashMapper` using Spring's {spring-framework-javadoc}/org/springframework/beans/BeanUtils.html[BeanUtils]. -* `ObjectHashMapper` using xref:redis/redis-repositories/mapping.adoc[Object-to-Hash Mapping]. +* javadoc:org.springframework.data.redis.hash.BeanUtilsHashMapper[] using Spring's {spring-framework-javadoc}/org/springframework/beans/BeanUtils.html[BeanUtils]. +* javadoc:org.springframework.data.redis.hash.ObjectHashMapper[] using xref:redis/redis-repositories/mapping.adoc[Object-to-Hash Mapping]. * <> using https://github.com/FasterXML/jackson[FasterXML Jackson]. The following example shows one way to implement hash mapping: @@ -53,7 +53,7 @@ public class HashMapping { [[redis.hashmappers.jackson2]] === Jackson2HashMapper -`Jackson2HashMapper` provides Redis Hash mapping for domain objects by using https://github.com/FasterXML/jackson[FasterXML Jackson]. +javadoc:org.springframework.data.redis.hash.Jackson2HashMapper[] provides Redis Hash mapping for domain objects by using https://github.com/FasterXML/jackson[FasterXML Jackson]. `Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. diff --git a/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc b/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc index 0fdc427c58..031425a4ed 100644 --- a/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc @@ -66,13 +66,13 @@ In order to subscribe to messages, one needs to implement the `MessageListener` [[redis:pubsub:subscribe:containers]] === Message Listener Containers -Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `RedisMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). +Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[], which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). -`RedisMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Redis channel and drive the `MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. +javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] acts as a message listener container. It is used to receive messages from a Redis channel and drive the javadoc:org.springframework.data.redis.connection.MessageListener[] instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. -A `MessageListener` can additionally implement `SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. +A javadoc:org.springframework.data.redis.connection.MessageListener[] can additionally implement javadoc:org.springframework.data.redis.connection.SubscriptionListener[] to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. -Furthermore, to minimize the application footprint, `RedisMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. +Furthermore, to minimize the application footprint, javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. To help with the asynchronous nature of messages, the container requires a `java.util.concurrent.Executor` (or Spring's `TaskExecutor`) for dispatching the messages. Depending on the load, the number of listeners, or the runtime environment, you should change or tweak the executor to better serve your needs. In particular, in managed environments (such as app servers), it is highly recommended to pick a proper `TaskExecutor` to take advantage of its runtime. @@ -80,7 +80,7 @@ To help with the asynchronous nature of messages, the container requires a `java [[redis:pubsub:subscribe:adapter]] === The MessageListenerAdapter -The `MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). +The javadoc:org.springframework.data.redis.listener.adapter.MessageListenerAdapter[] class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). Consider the following interface definition: @@ -96,7 +96,7 @@ public interface MessageDelegate { } ---- -Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: +Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the javadoc:org.springframework.data.redis.listener.adapter.MessageListenerAdapter[] class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: [source,java] ---- @@ -105,7 +105,7 @@ public class DefaultMessageDelegate implements MessageDelegate { } ---- -Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Redis dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: + Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Redis dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: [tabs] ====== @@ -162,7 +162,7 @@ XML:: ---- ====== -NOTE: The listener topic can be either a channel (for example, `topic="chatroom"`) or a pattern (for example, `topic="*room"`) +NOTE: The listener topic can be either a channel (for example, `topic="chatroom"` respective `Topic.channel("chatroom")`) or a pattern (for example, `topic="*room"` respective `Topic.pattern("*room")`). The preceding example uses the Redis namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: @@ -193,7 +193,7 @@ Each time a message is received, the adapter automatically and transparently per [[redis:reactive:pubsub:subscribe:containers]] == Reactive Message Listener Container -Spring Data offers `ReactiveRedisMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. +Spring Data offers javadoc:org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer[] which does all the heavy lifting of conversion and subscription state management on behalf of the user. The message listener container itself does not require external threading resources. It uses the driver threads to publish messages. @@ -223,7 +223,7 @@ stream.doOnNext(inner -> // notification hook when Redis subscriptions are synch [[redis:reactive:pubsub:subscribe:template]] === Subscribing via template API -As mentioned above you can directly use `ReactiveRedisTemplate` to subscribe to channels / patterns. This approach +As mentioned above you can directly use javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[] to subscribe to channels / patterns. This approach offers a straight forward, though limited solution as you lose the option to add subscriptions after the initial ones. Nevertheless you still can control the message stream via the returned `Flux` using eg. `take(Duration)`. When done reading, on error or cancellation all bound resources are freed again. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc index c965f352a9..4e9f48df54 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc @@ -2,7 +2,7 @@ = Redis Cache Spring Data Redis provides an implementation of Spring Framework's {spring-framework-docs}/integration.html#cache[Cache Abstraction] in the `org.springframework.data.redis.cache` package. -To use Redis as a backing implementation, add `RedisCacheManager` to your configuration, as follows: +To use Redis as a backing implementation, add javadoc:org.springframework.data.redis.cache.RedisCacheManager[] to your configuration, as follows: [source,java] ---- @@ -12,7 +12,7 @@ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) } ---- -`RedisCacheManager` behavior can be configured with `RedisCacheManagerBuilder`, letting you set the default `RedisCacheConfiguration`, transaction behavior, and predefined caches. +`RedisCacheManager` behavior can be configured with javadoc:org.springframework.data.redis.cache.RedisCacheManager$RedisCacheManagerBuilder[], letting you set the default javadoc:org.springframework.data.redis.cache.RedisCacheManager[], transaction behavior, and predefined caches. [source,java] ---- @@ -26,7 +26,7 @@ RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) As shown in the preceding example, `RedisCacheManager` allows custom configuration on a per-cache basis. -The behavior of `RedisCache` created by `RedisCacheManager` is defined with `RedisCacheConfiguration`. +The behavior of javadoc:org.springframework.data.redis.cache.RedisCache[] created by javadoc:org.springframework.data.redis.cache.RedisCacheManager[] is defined with `RedisCacheConfiguration`. The configuration lets you set key expiration times, prefixes, and `RedisSerializer` implementations for converting to and from the binary storage format, as shown in the following example: [source,java] @@ -36,7 +36,7 @@ RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCach .disableCachingNullValues(); ---- -`RedisCacheManager` defaults to a lock-free `RedisCacheWriter` for reading and writing binary values. +javadoc:org.springframework.data.redis.cache.RedisCacheManager[] defaults to a lock-free javadoc:org.springframework.data.redis.cache.RedisCacheWriter[] for reading and writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Redis. The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. @@ -47,8 +47,8 @@ It is possible to opt in to the locking behavior as follows: [source,java] ---- -RedisCacheManager cacheMangager = RedisCacheManager - .build(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) +RedisCacheManager cacheManager = RedisCacheManager + .builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) ... ---- @@ -77,7 +77,7 @@ The `SCAN` strategy requires a batch size to avoid excessive Redis command round [source,java] ---- RedisCacheManager cacheManager = RedisCacheManager - .build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) + .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) ... ---- diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc index d841506565..bd36cb1c59 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc @@ -6,7 +6,7 @@ Spring offers sophisticated for creating bean instances. Spring Data Redis ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. The extension is part of the JAR, so, to activate it, drop the Spring Data Redis JAR into your classpath. -You can then set up the infrastructure by implementing a CDI Producer for the `RedisConnectionFactory` and `RedisOperations`, as shown in the following example: +You can then set up the infrastructure by implementing a CDI Producer for the javadoc:org.springframework.data.redis.connection.RedisConnectionFactory[] and javadoc:org.springframework.data.redis.core.RedisOperations[], as shown in the following example: [source,java] ---- @@ -62,7 +62,7 @@ class RepositoryClient { } ---- -A Redis Repository requires `RedisKeyValueAdapter` and `RedisKeyValueTemplate` instances. +A Redis Repository requires javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter[] and javadoc:org.springframework.data.redis.core.RedisKeyValueTemplate[] instances. These beans are created and managed by the Spring Data CDI extension if no provided beans are found. -You can, however, supply your own beans to configure the specific properties of `RedisKeyValueAdapter` and `RedisKeyValueTemplate`. +You can, however, supply your own beans to configure the specific properties of javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter[] and javadoc:org.springframework.data.redis.core.RedisKeyValueTemplate[]. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc index 2ca39dee6d..2aa5e5eac5 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc @@ -13,7 +13,6 @@ The following table shows the details of data on a cluster (based on previous ex |people:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|15171|127.0.0.1:7381 |people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|7373|127.0.0.1:7380 |people:firstname:rand|index|1700|127.0.0.1:7379 -| |=============== ==== @@ -29,7 +28,6 @@ The following table shows what happens when you do (note the change in the slot |\{people}:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|2399|127.0.0.1:7379 |\{people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|2399|127.0.0.1:7379 |\{people}:firstname:rand|index|2399|127.0.0.1:7379 -| |=============== ==== diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc index 4372f7749b..ddb794213d 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc @@ -3,7 +3,7 @@ Objects stored in Redis may be valid only for a certain amount of time. This is especially useful for persisting short-lived objects in Redis without having to remove them manually when they reach their end of life. -The expiration time in seconds can be set with `@RedisHash(timeToLive=...)` as well as by using `KeyspaceSettings` (see xref:redis/redis-repositories/keyspaces.adoc[Keyspaces]). +The expiration time in seconds can be set with `@RedisHash(timeToLive=...)` as well as by using javadoc:org.springframework.data.redis.core.convert.KeyspaceConfiguration$KeyspaceSettings[] (see xref:redis/redis-repositories/keyspaces.adoc[Keyspaces]). More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or a method. However, do not apply `@TimeToLive` on both a method and a property within the same class. @@ -37,16 +37,16 @@ public class TimeToLiveOnMethod { NOTE: Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no associated expiration. -The repository implementation ensures subscription to https://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`. +The repository implementation ensures subscription to https://redis.io/topics/notifications[Redis keyspace notifications] via javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[]. When the expiration is set to a positive value, the corresponding `EXPIRE` command is run. In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. -This is done to enable the Repository support to publish `RedisKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. +This is done to enable the Repository support to publish javadoc:org.springframework.data.redis.core.RedisKeyExpiredEvent[], holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. Expiry events are received on all connected applications that use Spring Data Redis repositories. By default, the key expiry listener is disabled when initializing the application. The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. -See https://docs.spring.io/spring-data/redis/docs/{version}/api/org/springframework/data/redis/core/RedisKeyValueAdapter.EnableKeyspaceEvents.html[`EnableKeyspaceEvents`] for possible values. +See javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter$EnableKeyspaceEvents[] for possible values. The `RedisKeyExpiredEvent` holds a copy of the expired domain object as well as the key. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc index ca6a9c41b1..e7f4fae343 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc @@ -87,7 +87,7 @@ public void basicCrudOperations() { } ---- -<1> Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Redis Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2024-beeb-c666fab4c0e5`. +<1> Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Redis Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2025-beeb-c666fab4c0e5`. <2> Uses the provided `id` to retrieve the object stored at `keyspace:id`. <3> Counts the total number of entities available within the keyspace, `people`, defined by `@RedisHash` on `Person`. <4> Removes the key for the given object from Redis. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc index 97febc2cee..25d916f637 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc @@ -73,8 +73,8 @@ Due to its blocking nature, low-level polling is not attractive, as it requires Spring Data ships with two implementations tailored to the used programming model: -* `StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Redis Stream and drive the `StreamListener` instances that are injected into it. -* `StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Redis Stream as potentially infinite stream and emit stream messages through a `Flux`. +* javadoc:org.springframework.data.redis.stream.StreamMessageListenerContainer[] acts as message listener container for imperative programming models. It is used to consume records from a Redis Stream and drive the javadoc:org.springframework.data.redis.stream.StreamListener[] instances that are injected into it. +* javadoc:org.springframework.data.redis.stream.StreamReceiver[] provides a reactive variant of a message listener. It is used to consume messages from a Redis Stream as potentially infinite stream and emit stream messages through a `Flux`. `StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. @@ -84,7 +84,7 @@ Both containers allow runtime configuration changes so that you can add or remov [[imperative-streammessagelistenercontainer]] ==== Imperative `StreamMessageListenerContainer` -In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `org.springframework.data.redis.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. +In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the javadoc:org.springframework.data.redis.stream.StreamListener[] interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. [source,java] ---- diff --git a/src/main/antora/modules/ROOT/pages/redis/scripting.adoc b/src/main/antora/modules/ROOT/pages/redis/scripting.adoc index 38864e6e99..42411fbd9c 100644 --- a/src/main/antora/modules/ROOT/pages/redis/scripting.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/scripting.adoc @@ -3,9 +3,9 @@ Redis versions 2.6 and higher provide support for running Lua scripts through the https://redis.io/commands/eval[eval] and https://redis.io/commands/evalsha[evalsha] commands. Spring Data Redis provides a high-level abstraction for running scripts that handles serialization and automatically uses the Redis script cache. -Scripts can be run by calling the `execute` methods of `RedisTemplate` and `ReactiveRedisTemplate`. Both use a configurable `ScriptExecutor` (or `ReactiveScriptExecutor`) to run the provided script. By default, the `ScriptExecutor` (or `ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. +Scripts can be run by calling the `execute` methods of `RedisTemplate` and `ReactiveRedisTemplate`. Both use a configurable javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] (or javadoc:org.springframework.data.redis.core.script.ReactiveScriptExecutor[]) to run the provided script. By default, the javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] (or javadoc:org.springframework.data.redis.core.script.ReactiveScriptExecutor[]) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. -The default `ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Redis script cache. +The default javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Redis script cache. The following example runs a common "`check-and-set`" scenario by using a Lua script. This is an ideal use case for a Redis script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. @@ -34,7 +34,7 @@ public class Example { RedisScript script; public boolean checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, singletonList("key"), asList(expectedValue, newValue)); + return redisOperations.execute(script, List.of("key"), expectedValue, newValue); } } ---- @@ -52,7 +52,7 @@ public class Example { RedisScript script; public Flux checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, singletonList("key"), asList(expectedValue, newValue)); + return redisOperations.execute(script, List.of("key"), expectedValue, newValue); } } ---- @@ -69,10 +69,10 @@ end return false ---- -The preceding code configures a `RedisScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). +The preceding code configures a javadoc:org.springframework.data.redis.core.script.RedisScript[] pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). TIP: It is ideal to configure a single instance of `DefaultRedisScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. -The `checkAndSet` method above then runs the scripts. Scripts can be run within a `SessionCallback` as part of a transaction or pipeline. See "`xref:redis/transactions.adoc[Redis Transactions]`" and "`xref:redis/pipelining.adoc[Pipelining]`" for more information. +The `checkAndSet` method above then runs the scripts. Scripts can be run within a javadoc:org.springframework.data.redis.core.SessionCallback[] as part of a transaction or pipeline. See "`xref:redis/transactions.adoc[Redis Transactions]`" and "`xref:redis/pipelining.adoc[Pipelining]`" for more information. The scripting support provided by Spring Data Redis also lets you schedule Redis scripts for periodic running by using the Spring Task and Scheduler abstractions. See the https://spring.io/projects/spring-framework/[Spring Framework] documentation for more details. diff --git a/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc b/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc index 14ae2a6ac9..e363bb4f53 100644 --- a/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc @@ -4,11 +4,11 @@ Package `org.springframework.data.redis.support` offers various reusable components that rely on Redis as a backing store. Currently, the package contains various JDK-based interface implementations on top of Redis, such as https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html[atomic] counters and JDK https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html[Collections]. -NOTE: `RedisList` is forward-compatible with Java 21 `SequencedCollection`. +NOTE: javadoc:org.springframework.data.redis.support.collections.RedisList[] is forward-compatible with Java 21 `SequencedCollection`. The atomic counters make it easy to wrap Redis key incrementation while the collections allow easy management of Redis keys with minimal storage exposure or API leakage. -In particular, the `RedisSet` and `RedisZSet` interfaces offer easy access to the set operations supported by Redis, such as `intersection` and `union`. `RedisList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. -The following example shows the configuration for a bean that uses a `RedisList`: +In particular, the javadoc:org.springframework.data.redis.support.collections.RedisSet[] and javadoc:org.springframework.data.redis.support.collections.RedisZSet[] interfaces offer easy access to the set operations supported by Redis, such as `intersection` and `union`. javadoc:org.springframework.data.redis.support.collections.RedisList[] implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. +The following example shows the configuration for a bean that uses a javadoc:org.springframework.data.redis.support.collections.RedisList[]: [tabs] ====== diff --git a/src/main/antora/modules/ROOT/pages/redis/template.adoc b/src/main/antora/modules/ROOT/pages/redis/template.adoc index 38dd0d42e9..05af2e789b 100644 --- a/src/main/antora/modules/ROOT/pages/redis/template.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/template.adoc @@ -1,12 +1,12 @@ [[redis:template]] = Working with Objects through `RedisTemplate` -Most users are likely to use `RedisTemplate` and its corresponding package, `org.springframework.data.redis.core` or its reactive variant `ReactiveRedisTemplate`. +Most users are likely to use javadoc:org.springframework.data.redis.core.RedisTemplate[] and its corresponding package, `org.springframework.data.redis.core` or its reactive variant javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[]. The template is, in fact, the central class of the Redis module, due to its rich feature set. The template offers a high-level abstraction for Redis interactions. While `[Reactive]RedisConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. -The `RedisTemplate` class implements the `RedisOperations` interface and its reactive variant `ReactiveRedisTemplate` implements `ReactiveRedisOperations`. +The javadoc:org.springframework.data.redis.core.RedisTemplate[] class implements the javadoc:org.springframework.data.redis.core.RedisOperations[] interface and its reactive variant javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[] implements javadoc:org.springframework.data.redis.core.ReactiveRedisOperations[]. NOTE: The preferred way to reference operations on a `[Reactive]RedisTemplate` instance is through the `[Reactive]RedisOperations` interface. @@ -27,48 +27,48 @@ Imperative:: 2+^|_Key Type Operations_ -|`GeoOperations` +|javadoc:org.springframework.data.redis.core.GeoOperations[] |Redis geospatial operations, such as `GEOADD`, `GEORADIUS`,... -|`HashOperations` +|javadoc:org.springframework.data.redis.core.HashOperations[] |Redis hash operations -|`HyperLogLogOperations` +|javadoc:org.springframework.data.redis.core.HyperLogLogOperations[] |Redis HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... -|`ListOperations` +|javadoc:org.springframework.data.redis.core.ListOperations[] |Redis list operations -|`SetOperations` +|javadoc:org.springframework.data.redis.core.SetOperations[] |Redis set operations -|`ValueOperations` +|javadoc:org.springframework.data.redis.core.ValueOperations[] |Redis string (or value) operations -|`ZSetOperations` +|javadoc:org.springframework.data.redis.core.ZSetOperations[] |Redis zset (or sorted set) operations 2+^|_Key Bound Operations_ -|`BoundGeoOperations` +|javadoc:org.springframework.data.redis.core.BoundGeoOperations[] |Redis key bound geospatial operations -|`BoundHashOperations` +|javadoc:org.springframework.data.redis.core.BoundHashOperations[] |Redis hash key bound operations -|`BoundKeyOperations` +|javadoc:org.springframework.data.redis.core.BoundKeyOperations[] |Redis key bound operations -|`BoundListOperations` +|javadoc:org.springframework.data.redis.core.BoundListOperations[] |Redis list key bound operations -|`BoundSetOperations` +|javadoc:org.springframework.data.redis.core.BoundSetOperations[] |Redis set key bound operations -|`BoundValueOperations` +|javadoc:org.springframework.data.redis.core.BoundValueOperations[] |Redis string (or value) key bound operations -|`BoundZSetOperations` +|javadoc:org.springframework.data.redis.core.BoundZSetOperations[] |Redis zset (or sorted set) key bound operations |==== @@ -82,25 +82,25 @@ Reactive:: 2+^|_Key Type Operations_ -|`ReactiveGeoOperations` +|javadoc:org.springframework.data.redis.core.ReactiveGeoOperations[] |Redis geospatial operations such as `GEOADD`, `GEORADIUS`, and others) -|`ReactiveHashOperations` +|javadoc:org.springframework.data.redis.core.ReactiveHashOperations[] |Redis hash operations -|`ReactiveHyperLogLogOperations` +|javadoc:org.springframework.data.redis.core.ReactiveHyperLogLogOperations[] |Redis HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) -|`ReactiveListOperations` +|javadoc:org.springframework.data.redis.core.ReactiveListOperations[] |Redis list operations -|`ReactiveSetOperations` +|javadoc:org.springframework.data.redis.core.ReactiveSetOperations[] |Redis set operations -|`ReactiveValueOperations` +|javadoc:org.springframework.data.redis.core.ReactiveValueOperations[] |Redis string (or value) operations -|`ReactiveZSetOperations` +|javadoc:org.springframework.data.redis.core.ReactiveZSetOperations[] |Redis zset (or sorted set) operations |==== ====== @@ -159,7 +159,7 @@ class MyConfig { } @Bean - ReactiveRedisTemplate ReactiveRedisTemplate(ReactoveRedisConnectionFactory connectionFactory) { + ReactiveRedisTemplate ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) { return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string()); } } @@ -355,24 +355,24 @@ In Spring Data, the conversion between the user (custom) types and raw data (and This package contains two types of serializers that, as the name implies, take care of the serialization process: -* Two-way serializers based on ``RedisSerializer``. +* Two-way serializers based on javadoc:org.springframework.data.redis.serializer.RedisSerializer[]. * Element readers and writers that use `RedisElementReader` and ``RedisElementWriter``. The main difference between these variants is that `RedisSerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. Multiple implementations are available (including two that have been already mentioned in this documentation): -* `JdkSerializationRedisSerializer`, which is used by default for `RedisCache` and `RedisTemplate`. +* javadoc:org.springframework.data.redis.serializer.JdkSerializationRedisSerializer[], which is used by default for javadoc:org.springframework.data.redis.cache.RedisCache[] and javadoc:org.springframework.data.redis.core.RedisTemplate[]. * the `StringRedisSerializer`. -However, one can use `OxmSerializer` for Object/XML mapping through Spring {spring-framework-docs}/data-access.html#oxm[OXM] support or `Jackson2JsonRedisSerializer` or `GenericJackson2JsonRedisSerializer` for storing data in https://en.wikipedia.org/wiki/JSON[JSON] format. +However, one can use `OxmSerializer` for Object/XML mapping through Spring {spring-framework-docs}/data-access.html#oxm[OXM] support or javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] or javadoc:org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer[] for storing data in https://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values. It can be used for keys, values, or hashes without any restrictions. [WARNING] ==== -By default, `RedisCache` and `RedisTemplate` are configured to use Java native serialization. +By default, javadoc:org.springframework.data.redis.cache.RedisCache[] and javadoc:org.springframework.data.redis.core.RedisTemplate[] are configured to use Java native serialization. Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. Manipulated input could lead to unwanted code being run in the application during the deserialization step. As a consequence, do not use serialization in untrusted environments. diff --git a/src/main/antora/modules/ROOT/pages/redis/transactions.adoc b/src/main/antora/modules/ROOT/pages/redis/transactions.adoc index de48a55cc5..8ba20676bc 100644 --- a/src/main/antora/modules/ROOT/pages/redis/transactions.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/transactions.adoc @@ -2,10 +2,10 @@ = Redis Transactions Redis provides support for https://redis.io/topics/transactions[transactions] through the `multi`, `exec`, and `discard` commands. -These operations are available on `RedisTemplate`. +These operations are available on javadoc:org.springframework.data.redis.core.RedisTemplate[]. However, `RedisTemplate` is not guaranteed to run all the operations in the transaction with the same connection. -Spring Data Redis provides the `SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Redis transactions.The following example uses the `multi` method: +Spring Data Redis provides the javadoc:org.springframework.data.redis.core.SessionCallback[] interface for use when multiple operations need to be performed with the same `connection`, such as when using Redis transactions.The following example uses the `multi` method: [source,java] ---- diff --git a/src/main/java/org/springframework/data/redis/ClusterRedirectException.java b/src/main/java/org/springframework/data/redis/ClusterRedirectException.java index 79a5f0632d..562347e86a 100644 --- a/src/main/java/org/springframework/data/redis/ClusterRedirectException.java +++ b/src/main/java/org/springframework/data/redis/ClusterRedirectException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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.redis; +import java.io.Serial; + import org.springframework.dao.DataRetrievalFailureException; /** @@ -27,6 +29,7 @@ */ public class ClusterRedirectException extends DataRetrievalFailureException { + @Serial private static final long serialVersionUID = -857075813794333965L; private final int slot; @@ -43,7 +46,7 @@ public class ClusterRedirectException extends DataRetrievalFailureException { */ public ClusterRedirectException(int slot, String targetHost, int targetPort, Throwable e) { - super(String.format("Redirect: slot %s to %s:%s.", slot, targetHost, targetPort), e); + super("Redirect: slot %s to %s:%s.".formatted(slot, targetHost, targetPort), e); this.slot = slot; this.host = targetHost; diff --git a/src/main/java/org/springframework/data/redis/ClusterStateFailureException.java b/src/main/java/org/springframework/data/redis/ClusterStateFailureException.java index 61ccabde15..29543870d6 100644 --- a/src/main/java/org/springframework/data/redis/ClusterStateFailureException.java +++ b/src/main/java/org/springframework/data/redis/ClusterStateFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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.redis; +import java.io.Serial; + import org.springframework.dao.DataAccessResourceFailureException; /** @@ -28,6 +30,7 @@ */ public class ClusterStateFailureException extends DataAccessResourceFailureException { + @Serial private static final long serialVersionUID = 333399051713240852L; /** diff --git a/src/main/java/org/springframework/data/redis/ExceptionTranslationStrategy.java b/src/main/java/org/springframework/data/redis/ExceptionTranslationStrategy.java index d5c34e9cdb..a389a4c814 100644 --- a/src/main/java/org/springframework/data/redis/ExceptionTranslationStrategy.java +++ b/src/main/java/org/springframework/data/redis/ExceptionTranslationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/FallbackExceptionTranslationStrategy.java b/src/main/java/org/springframework/data/redis/FallbackExceptionTranslationStrategy.java index ed9728797f..58eb89e21b 100644 --- a/src/main/java/org/springframework/data/redis/FallbackExceptionTranslationStrategy.java +++ b/src/main/java/org/springframework/data/redis/FallbackExceptionTranslationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/PassThroughExceptionTranslationStrategy.java b/src/main/java/org/springframework/data/redis/PassThroughExceptionTranslationStrategy.java index aa4aeee609..55da37766b 100644 --- a/src/main/java/org/springframework/data/redis/PassThroughExceptionTranslationStrategy.java +++ b/src/main/java/org/springframework/data/redis/PassThroughExceptionTranslationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/RedisConnectionFailureException.java b/src/main/java/org/springframework/data/redis/RedisConnectionFailureException.java index 73d4fe2045..044a13126b 100644 --- a/src/main/java/org/springframework/data/redis/RedisConnectionFailureException.java +++ b/src/main/java/org/springframework/data/redis/RedisConnectionFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/RedisSystemException.java b/src/main/java/org/springframework/data/redis/RedisSystemException.java index 50d2218dd3..64230b481f 100644 --- a/src/main/java/org/springframework/data/redis/RedisSystemException.java +++ b/src/main/java/org/springframework/data/redis/RedisSystemException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -16,6 +16,7 @@ package org.springframework.data.redis; import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.lang.Nullable; /** * Exception thrown when we can't classify a Redis exception into one of Spring generic data access exceptions. @@ -28,7 +29,7 @@ public class RedisSystemException extends UncategorizedDataAccessException { * @param msg the detail message. * @param cause the root cause from the data access API in use. */ - public RedisSystemException(String msg, Throwable cause) { + public RedisSystemException(String msg, @Nullable Throwable cause) { super(msg, cause); } diff --git a/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java b/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java index 6a40b28f00..e985374ae7 100644 --- a/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java +++ b/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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.redis; +import java.io.Serial; + import org.springframework.dao.DataRetrievalFailureException; /** @@ -25,6 +27,7 @@ */ public class TooManyClusterRedirectionsException extends DataRetrievalFailureException { + @Serial private static final long serialVersionUID = -2818933672669154328L; /** diff --git a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java index 0649d513e6..efa2669e3c 100644 --- a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java +++ b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/cache/BatchStrategies.java b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java index 047ef3834c..dc808e516e 100644 --- a/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java +++ b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/cache/BatchStrategy.java b/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java index d18db54c9a..98b3b32360 100644 --- a/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java +++ b/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/cache/CacheKeyPrefix.java b/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java index a08003f2d2..6d340afdad 100644 --- a/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java +++ b/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/cache/CacheStatistics.java b/src/main/java/org/springframework/data/redis/cache/CacheStatistics.java index d69d768508..7e92fa7835 100644 --- a/src/main/java/org/springframework/data/redis/cache/CacheStatistics.java +++ b/src/main/java/org/springframework/data/redis/cache/CacheStatistics.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/CacheStatisticsCollector.java b/src/main/java/org/springframework/data/redis/cache/CacheStatisticsCollector.java index 24b5eff794..d0ab23a6e3 100644 --- a/src/main/java/org/springframework/data/redis/cache/CacheStatisticsCollector.java +++ b/src/main/java/org/springframework/data/redis/cache/CacheStatisticsCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/CacheStatisticsProvider.java b/src/main/java/org/springframework/data/redis/cache/CacheStatisticsProvider.java index b7566f4212..2a4eed63de 100644 --- a/src/main/java/org/springframework/data/redis/cache/CacheStatisticsProvider.java +++ b/src/main/java/org/springframework/data/redis/cache/CacheStatisticsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/DefaultCacheStatisticsCollector.java b/src/main/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollector.java index 6a8d5b7620..693a8e945b 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollector.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index 205487fa1b..7d5b291404 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,7 +24,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.data.redis.connection.ReactiveRedisConnection; @@ -137,9 +139,14 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) { Assert.notNull(name, "Name must not be null"); Assert.notNull(key, "Key must not be null"); - byte[] result = shouldExpireWithin(ttl) - ? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl))) - : execute(name, connection -> connection.stringCommands().get(key)); + return execute(name, connection -> doGet(connection, name, key, ttl)); + } + + @Nullable + private byte[] doGet(RedisConnection connection, String name, byte[] key, @Nullable Duration ttl) { + + byte[] result = shouldExpireWithin(ttl) ? connection.stringCommands().getEx(key, Expiration.from(ttl)) + : connection.stringCommands().get(key); statistics.incGets(name); @@ -152,6 +159,48 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) { return result; } + @Override + public byte[] get(String name, byte[] key, Supplier valueLoader, @Nullable Duration ttl, + boolean timeToIdleEnabled) { + + Assert.notNull(name, "Name must not be null"); + Assert.notNull(key, "Key must not be null"); + + boolean withTtl = shouldExpireWithin(ttl); + + // double-checked locking optimization + if (isLockingCacheWriter()) { + byte[] bytes = get(name, key, timeToIdleEnabled && withTtl ? ttl : null); + if (bytes != null) { + return bytes; + } + } + + return execute(name, connection -> { + + if (isLockingCacheWriter()) { + doLock(name, key, null, connection); + } + + try { + + byte[] result = doGet(connection, name, key, timeToIdleEnabled && withTtl ? ttl : null); + + if (result != null) { + return result; + } + + byte[] value = valueLoader.get(); + doPut(connection, name, key, value, ttl); + return value; + } finally { + if (isLockingCacheWriter()) { + doUnlock(name, connection); + } + } + }); + } + @Override public boolean supportsAsyncRetrieve() { return asyncCacheWriter.isSupported(); @@ -186,17 +235,21 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { Assert.notNull(value, "Value must not be null"); execute(name, connection -> { - - if (shouldExpireWithin(ttl)) { - connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), - SetOption.upsert()); - } else { - connection.stringCommands().set(key, value); - } - + doPut(connection, name, key, value, ttl); return "OK"; }); + } + + private void doPut(RedisConnection connection, String name, byte[] key, byte[] value, @Nullable Duration ttl) { + + if (shouldExpireWithin(ttl)) { + connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), + SetOption.upsert()); + } else { + connection.stringCommands().set(key, value); + } + statistics.incPuts(name); } @@ -220,10 +273,8 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat return execute(name, connection -> { - boolean wasLocked = false; if (isLockingCacheWriter()) { doLock(name, key, value, connection); - wasLocked = true; } try { @@ -245,7 +296,7 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat return connection.stringCommands().get(key); } finally { - if (isLockingCacheWriter() && wasLocked) { + if (isLockingCacheWriter()) { doUnlock(name, connection); } } @@ -270,12 +321,9 @@ public void clean(String name, byte[] pattern) { execute(name, connection -> { - boolean wasLocked = false; - try { if (isLockingCacheWriter()) { doLock(name, name, pattern, connection); - wasLocked = true; } long deleteCount = batchStrategy.cleanCache(connection, name, pattern); @@ -288,7 +336,7 @@ public void clean(String name, byte[] pattern) { statistics.incDeletesBy(name, (int) deleteCount); } finally { - if (wasLocked && isLockingCacheWriter()) { + if (isLockingCacheWriter()) { doUnlock(name, connection); } } @@ -319,10 +367,10 @@ public RedisCacheWriter withStatisticsCollector(CacheStatisticsCollector cacheSt * @param name the name of the cache to lock. */ void lock(String name) { - execute(name, connection -> doLock(name, name, null, connection)); + executeWithoutResult(name, connection -> doLock(name, name, null, connection)); } - boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) { + void doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) { RedisStringCommands commands = connection.stringCommands(); Expiration expiration = Expiration.from(this.lockTtl.getTimeToLive(contextualKey, contextualValue)); @@ -332,8 +380,6 @@ boolean doLock(String name, Object contextualKey, @Nullable Object contextualVal true)) { checkAndPotentiallyWaitUntilUnlocked(name, connection); } - - return true; } /** @@ -358,6 +404,14 @@ private T execute(String name, Function callback) { } } + private void executeWithoutResult(String name, Consumer callback) { + + try (RedisConnection connection = this.connectionFactory.getConnection()) { + checkAndPotentiallyWaitUntilUnlocked(name, connection); + callback.accept(connection); + } + } + private T executeLockFree(Function callback) { try (RedisConnection connection = this.connectionFactory.getConnection()) { @@ -391,9 +445,7 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c // Re-interrupt current Thread to allow other participants to react. Thread.currentThread().interrupt(); - String message = String.format("Interrupted while waiting to unlock cache %s", name); - - throw new PessimisticLockingFailureException(message, ex); + throw new PessimisticLockingFailureException("Interrupted while waiting to unlock cache %s".formatted(name), ex); } finally { this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs); } @@ -429,7 +481,7 @@ interface AsyncCacheWriter { * @param name the cache name from which to retrieve the cache entry. * @param key the cache entry key. * @param ttl optional TTL to set for Time-to-Idle eviction. - * @return a future that completes either with a value if the value exists or completing with {@code null} if the + * @return a future that completes either with a value if the value exists or completing with {@literal null} if the * cache does not contain an entry. */ CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl); diff --git a/src/main/java/org/springframework/data/redis/cache/FixedDurationTtlFunction.java b/src/main/java/org/springframework/data/redis/cache/FixedDurationTtlFunction.java index e0a328e878..6c456de4c8 100644 --- a/src/main/java/org/springframework/data/redis/cache/FixedDurationTtlFunction.java +++ b/src/main/java/org/springframework/data/redis/cache/FixedDurationTtlFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/cache/MutableCacheStatistics.java b/src/main/java/org/springframework/data/redis/cache/MutableCacheStatistics.java index a783da8c8f..94a19b1700 100644 --- a/src/main/java/org/springframework/data/redis/cache/MutableCacheStatistics.java +++ b/src/main/java/org/springframework/data/redis/cache/MutableCacheStatistics.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/NoOpCacheStatisticsCollector.java b/src/main/java/org/springframework/data/redis/cache/NoOpCacheStatisticsCollector.java index 498ff82679..4180ce875d 100644 --- a/src/main/java/org/springframework/data/redis/cache/NoOpCacheStatisticsCollector.java +++ b/src/main/java/org/springframework/data/redis/cache/NoOpCacheStatisticsCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java index 187add2512..8bf70e0853 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,8 +25,6 @@ import java.util.StringJoiner; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import org.springframework.cache.Cache; @@ -39,7 +37,6 @@ import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.util.ByteUtils; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -64,8 +61,6 @@ public class RedisCache extends AbstractValueAdaptingCache { static final String CACHE_RETRIEVAL_UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE = "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"; - private final Lock lock = new ReentrantLock(); - private final RedisCacheConfiguration cacheConfiguration; private final RedisCacheWriter cacheWriter; @@ -86,8 +81,7 @@ public class RedisCache extends AbstractValueAdaptingCache { */ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfiguration) { - super(RedisAssertions.requireNonNull(cacheConfiguration, "CacheConfiguration must not be null") - .getAllowCacheNullValues()); + super(cacheConfiguration.getAllowCacheNullValues()); Assert.notNull(name, "Name must not be null"); Assert.notNull(cacheWriter, "CacheWriter must not be null"); @@ -154,28 +148,18 @@ public CacheStatistics getStatistics() { @SuppressWarnings("unchecked") public T get(Object key, Callable valueLoader) { - ValueWrapper result = get(key); - - return result != null ? (T) result.get() : getSynchronized(key, valueLoader); - } - - @Nullable - @SuppressWarnings("unchecked") - private T getSynchronized(Object key, Callable valueLoader) { + byte[] binaryKey = createAndConvertCacheKey(key); + byte[] binaryValue = getCacheWriter().get(getName(), binaryKey, + () -> serializeCacheValue(toStoreValue(loadCacheValue(key, valueLoader))), getTimeToLive(key), + getCacheConfiguration().isTimeToIdleEnabled()); - lock.lock(); + ValueWrapper result = toValueWrapper(deserializeCacheValue(binaryValue)); - try { - ValueWrapper result = get(key); - return result != null ? (T) result.get() : loadCacheValue(key, valueLoader); - } finally { - lock.unlock(); - } + return result != null ? (T) result.get() : null; } /** - * Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts} the - * {@link Object loaded value} in the cache. + * Loads the {@link Object} using the given {@link Callable valueLoader}. * * @param {@link Class type} of the loaded {@link Object cache value}. * @param key {@link Object key} mapped to the loaded {@link Object cache value}. @@ -184,17 +168,11 @@ private T getSynchronized(Object key, Callable valueLoader) { */ protected T loadCacheValue(Object key, Callable valueLoader) { - T value; - try { - value = valueLoader.call(); + return valueLoader.call(); } catch (Exception ex) { throw new ValueRetrievalException(key, valueLoader, ex); } - - put(key, value); - - return value; } @Override @@ -318,12 +296,9 @@ private Object processAndCheckValue(@Nullable Object value) { Object cacheValue = preProcessCacheValue(value); if (nullCacheValueIsNotAllowed(cacheValue)) { - - String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null" + throw new IllegalArgumentException(("Cache '%s' does not allow 'null' values; Avoid storing null" + " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'" - + " via RedisCacheConfiguration", getName()); - - throw new IllegalArgumentException(message); + + " via RedisCacheConfiguration").formatted(getName())); } return cacheValue; @@ -434,16 +409,18 @@ protected String convertKey(Object key) { return key.toString(); } - String message = String.format( - "Cannot convert cache key %s to String; Please register a suitable Converter" - + " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'", - source, key.getClass().getName()); - - throw new IllegalStateException(message); + throw new IllegalStateException(("Cannot convert cache key %s to String; Please register a suitable Converter" + + " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'") + .formatted(source, key.getClass().getName())); } private CompletableFuture retrieveValue(Object key) { - return getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key)) // + + CompletableFuture retrieve = getCacheConfiguration().isTimeToIdleEnabled() + ? getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key), getTimeToLive(key)) + : getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key)); + + return retrieve // .thenApply(binaryValue -> binaryValue != null ? deserializeCacheValue(binaryValue) : null) // .thenApply(this::toValueWrapper); } @@ -499,7 +476,7 @@ private String convertCollectionLikeOrMapKey(Object key, TypeDescriptor source) return "[" + stringJoiner + "]"; } - throw new IllegalArgumentException(String.format("Cannot convert cache key [%s] to String", key)); + throw new IllegalArgumentException("Cannot convert cache key [%s] to String".formatted(key)); } private byte[] createAndConvertCacheKey(Object key) { diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java index 261c932ab2..b74ebf2d75 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -424,11 +424,9 @@ public void addCacheKeyConverter(Converter cacheKeyConverter) { public void configureKeyConverters(Consumer registryConsumer) { if (!(getConversionService() instanceof ConverterRegistry)) { - - String message = "'%s' returned by getConversionService() does not allow Converter registration;" - + " Please make sure to provide a ConversionService that implements ConverterRegistry"; - - throw new IllegalStateException(String.format(message, getConversionService().getClass().getName())); + throw new IllegalStateException(("'%s' returned by getConversionService() does not allow Converter registration;" + + " Please make sure to provide a ConversionService that implements ConverterRegistry") + .formatted(getConversionService().getClass().getName())); } registryConsumer.accept((ConverterRegistry) getConversionService()); @@ -438,14 +436,13 @@ public void configureKeyConverters(Consumer registryConsumer) * Registers default cache {@link Converter key converters}. *

* The following converters get registered: - *

*

    - *
  • {@link String} to {@link byte byte[]} using UTF-8 encoding.
  • + *
  • {@link String} to {@code byte byte[]} using UTF-8 encoding.
  • *
  • {@link SimpleKey} to {@link String}
  • *
* - * @param registry {@link ConverterRegistry} in which the {@link Converter key converters} are registered; - * must not be {@literal null}. + * @param registry {@link ConverterRegistry} in which the {@link Converter key converters} are registered; must not be + * {@literal null}. * @see org.springframework.core.convert.converter.ConverterRegistry */ public static void registerDefaultConverters(ConverterRegistry registry) { diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java index 1df8208ef4..3c9e3c5415 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -27,7 +27,6 @@ import org.springframework.cache.CacheManager; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -103,10 +102,11 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowRuntimeCacheCreation) { - this.defaultCacheConfiguration = RedisAssertions.requireNonNull(defaultCacheConfiguration, - "DefaultCacheConfiguration must not be null"); + Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null"); + Assert.notNull(cacheWriter, "CacheWriter must not be null"); - this.cacheWriter = RedisAssertions.requireNonNull(cacheWriter, "CacheWriter must not be null"); + this.defaultCacheConfiguration = defaultCacheConfiguration; + this.cacheWriter = cacheWriter; this.initialCacheConfiguration = new LinkedHashMap<>(); this.allowRuntimeCacheCreation = allowRuntimeCacheCreation; } @@ -423,7 +423,10 @@ public static class RedisCacheManagerBuilder { * @see org.springframework.data.redis.cache.RedisCacheWriter */ public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) { - return new RedisCacheManagerBuilder(RedisAssertions.requireNonNull(cacheWriter, "CacheWriter must not be null")); + + Assert.notNull(cacheWriter, "CacheWriter must not be null"); + + return new RedisCacheManagerBuilder(cacheWriter); } /** @@ -534,7 +537,10 @@ public RedisCacheManagerBuilder cacheDefaults(RedisCacheConfiguration defaultCac * @since 2.3 */ public RedisCacheManagerBuilder cacheWriter(RedisCacheWriter cacheWriter) { - this.cacheWriter = RedisAssertions.requireNonNull(cacheWriter, "CacheWriter must not be null"); + + Assert.notNull(cacheWriter, "CacheWriter must not be null"); + + this.cacheWriter = cacheWriter; return this; } @@ -558,8 +564,10 @@ public RedisCacheManagerBuilder enableStatistics() { */ public RedisCacheManagerBuilder initialCacheNames(Set cacheNames) { - RedisAssertions.requireNonNull(cacheNames, "CacheNames must not be null") - .forEach(it -> withCacheConfiguration(it, defaultCacheConfiguration)); + Assert.notNull(cacheNames, "CacheNames must not be null"); + Assert.noNullElements(cacheNames, "CacheNames must not be null"); + + cacheNames.forEach(it -> withCacheConfiguration(it, defaultCacheConfiguration)); return this; } @@ -603,9 +611,9 @@ public RedisCacheManagerBuilder withCacheConfiguration(String cacheName, public RedisCacheManagerBuilder withInitialCacheConfigurations( Map cacheConfigurations) { - RedisAssertions.requireNonNull(cacheConfigurations, "CacheConfigurations must not be null") - .forEach((cacheName, cacheConfiguration) -> RedisAssertions.requireNonNull(cacheConfiguration, - "RedisCacheConfiguration for cache [%s] must not be null", cacheName)); + Assert.notNull(cacheConfigurations, "CacheConfigurations must not be null!"); + cacheConfigurations.forEach((cacheName, configuration) -> Assert.notNull(configuration, + String.format("RedisCacheConfiguration for cache %s must not be null!", cacheName))); this.initialCaches.putAll(cacheConfigurations); diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index 04dd0e3507..e5e3d16b2b 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,14 +24,14 @@ import org.springframework.util.Assert; /** - * {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) - * used for caching. + * {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for + * caching. *

* The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for reading/writing * binary data from/to Redis. The implementation honors potential cache lock flags that might be set. *

- * The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} - * to tune performance behavior. + * The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} to tune performance + * behavior. * * @author Christoph Strobl * @author Mark Paluch @@ -96,9 +96,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio * * @param connectionFactory must not be {@literal null}. * @param sleepTime sleep time between lock access attempts, must not be {@literal null}. - * @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys - * and values (such as the cache name on cleanup or the actual key/value on put requests); - * must not be {@literal null}. + * @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys and values + * (such as the cache name on cleanup or the actual key/value on put requests); must not be {@literal null}. * @param batchStrategy must not be {@literal null}. * @return new instance of {@link DefaultRedisCacheWriter}. * @since 3.2 @@ -124,8 +123,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio byte[] get(String name, byte[] key); /** - * Get the binary value representation from Redis stored for the given key and set - * the given {@link Duration TTL expiration} for the cache entry. + * Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL + * expiration} for the cache entry. * * @param name must not be {@literal null}. * @param key must not be {@literal null}. @@ -138,14 +137,41 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) { } /** - * Determines whether the asynchronous {@link #retrieve(String, byte[])} - * and {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation. + * Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL + * expiration} for the cache entry, obtaining the value from {@code valueLoader} if necessary. *

- * The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined - * by the Redis driver in use at runtime. + * If possible (and configured for locking), implementations should ensure that the loading operation is synchronized + * so that the specified {@code valueLoader} is only called once in case of concurrent access on the same key. + * + * @param name must not be {@literal null}. + * @param key must not be {@literal null}. + * @param valueLoader value loader that creates the value if the cache lookup has been not successful. + * @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry. + * @param timeToIdleEnabled {@literal true} to enable Time to Idle when retrieving the value. + * @since 3.4 + */ + default byte[] get(String name, byte[] key, Supplier valueLoader, @Nullable Duration ttl, + boolean timeToIdleEnabled) { + + byte[] bytes = timeToIdleEnabled ? get(name, key, ttl) : get(name, key); + + if (bytes == null) { + bytes = valueLoader.get(); + put(name, key, bytes, ttl); + } + + return bytes; + } + + /** + * Determines whether the asynchronous {@link #retrieve(String, byte[])} and + * {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation. + *

+ * The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined by the + * Redis driver in use at runtime. *

- * Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} - * and {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}. + * Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} and + * {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}. * * @return {@literal true} if asynchronous {@literal retrieve} operations are supported by the implementation. * @since 3.2 @@ -155,14 +181,14 @@ default boolean supportsAsyncRetrieve() { } /** - * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} - * maps the given {@link byte[] key}. + * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps the given + * {@code byte[] key}. *

* This operation is non-blocking. * * @param name {@link String} with the name of the {@link RedisCache}. - * @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. - * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + * @param key {@code byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. + * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@code byte[] key}. * @see #retrieve(String, byte[], Duration) * @since 3.2 */ @@ -171,15 +197,15 @@ default CompletableFuture retrieve(String name, byte[] key) { } /** - * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps - * the given {@link byte[] key} setting the {@link Duration TTL expiration} for the cache entry. + * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps the given + * {@code byte[] key} setting the {@link Duration TTL expiration} for the cache entry. *

* This operation is non-blocking. * * @param name {@link String} with the name of the {@link RedisCache}. - * @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. + * @param key {@code byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. * @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry. - * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@code byte[] key}. * @since 3.2 */ CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl); @@ -187,10 +213,10 @@ default CompletableFuture retrieve(String name, byte[] key) { /** * Write the given key/value pair to Redis and set the expiration time if defined. * - * @param name 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 ttl Optional expiration time. Can be {@literal null}. + * @param name cache name must not be {@literal null}. + * @param key key for the cache entry. Must not be {@literal null}. + * @param value value stored for the key. Must not be {@literal null}. + * @param ttl optional expiration time. Can be {@literal null}. */ void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); @@ -199,10 +225,10 @@ default CompletableFuture retrieve(String name, byte[] key) { *

* This operation is non-blocking. * - * @param name 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 ttl Optional expiration time. Can be {@literal null}. + * @param name cache name must not be {@literal null}. + * @param key key for the cache entry. Must not be {@literal null}. + * @param value value stored for the key. Must not be {@literal null}. + * @param ttl optional expiration time. Can be {@literal null}. * @since 3.2 */ CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl); @@ -210,10 +236,10 @@ default CompletableFuture retrieve(String name, byte[] key) { /** * Write the given value to Redis if the key does not already exist. * - * @param name 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 ttl Optional expiration time. Can be {@literal null}. + * @param name cache name must not be {@literal null}. + * @param key key for the cache entry. Must not be {@literal null}. + * @param value value stored for the key. Must not be {@literal null}. + * @param ttl optional expiration time. Can be {@literal null}. * @return {@literal null} if the value has been written, the value stored for the key if it already exists. */ @Nullable @@ -222,16 +248,16 @@ default CompletableFuture retrieve(String name, byte[] key) { /** * Remove the given key from Redis. * - * @param name The cache name must not be {@literal null}. - * @param key The key for the cache entry. Must not be {@literal null}. + * @param name cache name must not be {@literal null}. + * @param key key for the cache entry. Must not be {@literal null}. */ void remove(String name, byte[] key); /** * Remove all keys following the given pattern. * - * @param name The cache name must not be {@literal null}. - * @param pattern The pattern for the keys to remove. Must not be {@literal null}. + * @param name cache name must not be {@literal null}. + * @param pattern pattern for the keys to remove. Must not be {@literal null}. */ void clean(String name, byte[] pattern); @@ -264,8 +290,8 @@ interface TtlFunction { /** * Creates a {@literal Singleton} {@link TtlFunction} using the given {@link Duration}. * - * @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry - * does not expire). + * @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry does not + * expire). * @return a singleton {@link TtlFunction} using {@link Duration}. */ static TtlFunction just(Duration duration) { @@ -293,7 +319,7 @@ static TtlFunction persistent() { * persistent value that does not expire. * * @param key the cache key. - * @param value the cache value. Can be {@code null} if the cache supports {@code null} value caching. + * @param value the cache value. Can be {@literal null} if the cache supports {@literal null} value caching. * @return the computed {@link Duration time-to-live (TTL)}. Can be {@link Duration#ZERO} for persistent values * (i.e. cache entry does not expire). */ diff --git a/src/main/java/org/springframework/data/redis/config/RedisCollectionParser.java b/src/main/java/org/springframework/data/redis/config/RedisCollectionParser.java index 6e5963eb9f..bb50defa3e 100644 --- a/src/main/java/org/springframework/data/redis/config/RedisCollectionParser.java +++ b/src/main/java/org/springframework/data/redis/config/RedisCollectionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/config/RedisListenerContainerParser.java b/src/main/java/org/springframework/data/redis/config/RedisListenerContainerParser.java index 7c09a8896d..c69d645e7b 100644 --- a/src/main/java/org/springframework/data/redis/config/RedisListenerContainerParser.java +++ b/src/main/java/org/springframework/data/redis/config/RedisListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/config/RedisNamespaceHandler.java b/src/main/java/org/springframework/data/redis/config/RedisNamespaceHandler.java index 256dba0ffd..5c4394020a 100644 --- a/src/main/java/org/springframework/data/redis/config/RedisNamespaceHandler.java +++ b/src/main/java/org/springframework/data/redis/config/RedisNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/AbstractRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java index 7b2581973d..46a7a12ded 100644 --- a/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/BitFieldSubCommands.java b/src/main/java/org/springframework/data/redis/connection/BitFieldSubCommands.java index bb24d05634..b90121f90b 100644 --- a/src/main/java/org/springframework/data/redis/connection/BitFieldSubCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/BitFieldSubCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -151,10 +151,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof BitFieldSubCommands)) { + if (!(o instanceof BitFieldSubCommands that)) { return false; } - BitFieldSubCommands that = (BitFieldSubCommands) o; return ObjectUtils.nullSafeEquals(subCommands, that.subCommands); } @@ -437,10 +436,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof Offset)) { + if (!(o instanceof Offset that)) { return false; } - Offset that = (Offset) o; if (offset != that.offset) { return false; } @@ -549,10 +547,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof BitFieldType)) { + if (!(o instanceof BitFieldType that)) { return false; } - BitFieldType that = (BitFieldType) o; if (signed != that.signed) { return false; } @@ -597,10 +594,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof AbstractBitFieldSubCommand)) { + if (!(o instanceof AbstractBitFieldSubCommand that)) { return false; } - AbstractBitFieldSubCommand that = (AbstractBitFieldSubCommand) o; if (!ObjectUtils.nullSafeEquals(getClass(), that.getClass())) { return false; } @@ -648,13 +644,13 @@ public static class BitFieldSet extends AbstractBitFieldSubCommand { * @return * @since 2.5.2 */ - public static BitFieldSet create(BitFieldType type,Offset offset,long value){ + public static BitFieldSet create(BitFieldType type, Offset offset, long value) { Assert.notNull(type, "BitFieldType must not be null"); Assert.notNull(offset, "Offset must not be null"); BitFieldSet instance = new BitFieldSet(); - instance.type = type; + instance.type = type; instance.offset = offset; instance.value = value; @@ -680,13 +676,12 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof BitFieldSet)) { + if (!(o instanceof BitFieldSet that)) { return false; } if (!super.equals(o)) { return false; } - BitFieldSet that = (BitFieldSet) o; if (value != that.value) { return false; } @@ -728,13 +723,13 @@ public static class BitFieldGet extends AbstractBitFieldSubCommand { * @since 2.5.2 * @return */ - public static BitFieldGet create(BitFieldType type,Offset offset){ + public static BitFieldGet create(BitFieldType type, Offset offset) { Assert.notNull(type, "BitFieldType must not be null"); Assert.notNull(offset, "Offset must not be null"); BitFieldGet instance = new BitFieldGet(); - instance.type = type; + instance.type = type; instance.offset = offset; return instance; @@ -767,7 +762,7 @@ public static class BitFieldIncrBy extends AbstractBitFieldSubCommand { * @return * @since 2.5.2 */ - public static BitFieldIncrBy create(BitFieldType type,Offset offset,long value){ + public static BitFieldIncrBy create(BitFieldType type, Offset offset, long value) { return create(type, offset, value, null); } @@ -787,7 +782,7 @@ public static BitFieldIncrBy create(BitFieldType type, Offset offset, long value Assert.notNull(offset, "Offset must not be null"); BitFieldIncrBy instance = new BitFieldIncrBy(); - instance.type = type; + instance.type = type; instance.offset = offset; instance.value = value; instance.overflow = overflow; @@ -832,10 +827,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof BitFieldIncrBy)) { + if (!(o instanceof BitFieldIncrBy that)) { return false; } - BitFieldIncrBy that = (BitFieldIncrBy) o; if (value != that.value) { return false; } diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java index 7a786b63f9..1ee5458ec3 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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.redis.connection; +import java.io.Serial; import java.util.Collections; import java.util.List; @@ -29,7 +30,7 @@ */ public class ClusterCommandExecutionFailureException extends UncategorizedDataAccessException { - private static final long serialVersionUID = 5727044227040368955L; + @Serial private static final long serialVersionUID = 5727044227040368955L; /** * Creates new {@link ClusterCommandExecutionFailureException}. diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java index 3e896e53d9..b0d9c1a720 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,12 +130,9 @@ private NodeResult executeCommandOnSingleNode(ClusterCommandCallback this.maxRedirects) { - - String message = String.format("Cannot follow Cluster Redirects over more than %s legs; " - + "Consider increasing the number of redirects to follow; Current value is: %s.", - redirectCount, this.maxRedirects); - - throw new TooManyClusterRedirectionsException(message); + throw new TooManyClusterRedirectionsException(("Cannot follow Cluster Redirects over more than %s legs;" + + " Consider increasing the number of redirects to follow; Current value is: %s") + .formatted(redirectCount, this.maxRedirects)); } RedisClusterNode nodeToUse = lookupNode(node); @@ -178,7 +175,7 @@ private RedisClusterNode lookupNode(RedisClusterNode node) { try { return topologyProvider.getTopology().lookup(node); } catch (ClusterStateFailureException ex) { - throw new IllegalArgumentException(String.format("Node %s is unknown to cluster", node), ex); + throw new IllegalArgumentException("Node %s is unknown to cluster".formatted(node), ex); } } @@ -215,7 +212,7 @@ public MultiNodeResult executeCommandAsyncOnNodes(ClusterCommandCallba try { resolvedRedisClusterNodes.add(topology.lookup(node)); } catch (ClusterStateFailureException ex) { - throw new IllegalArgumentException(String.format("Node %s is unknown to cluster", node), ex); + throw new IllegalArgumentException("Node %s is unknown to cluster".formatted(node), ex); } } @@ -474,9 +471,9 @@ public RedisClusterNode getNode() { } /** - * Return the {@link byte[] key} mapped to the value stored in Redis. + * Return the {@code byte[] key} mapped to the value stored in Redis. * - * @return a {@link byte[] byte array} of the key mapped to the value stored in Redis. + * @return a {@code byte[] byte array} of the key mapped to the value stored in Redis. */ public byte[] getKey() { return this.key.getArray(); diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java b/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java index e09ebbfd8d..8d6179811c 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/ClusterNodeResourceProvider.java b/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java index 454ac41d23..a12ce3f8ea 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/ClusterSlotHashUtil.java b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java index 6494f423c7..58b49520bf 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/ClusterTopology.java b/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java index bb82facae2..42436431f4 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -141,7 +141,7 @@ public RedisClusterNode getKeyServingMasterNode(byte[] key) { } throw new ClusterStateFailureException( - String.format("Could not find master node serving slot %s for key '%s',", slot, Arrays.toString(key))); + "Could not find master node serving slot %s for key '%s',".formatted(slot, Arrays.toString(key))); } /** @@ -161,7 +161,7 @@ public RedisClusterNode lookup(String host, int port) { } throw new ClusterStateFailureException( - String.format("Could not find node at %s:%s; Is your cluster info up to date", host, port)); + "Could not find node at %s:%d; Is your cluster info up to date".formatted(host, port)); } /** @@ -182,7 +182,7 @@ public RedisClusterNode lookup(String nodeId) { } throw new ClusterStateFailureException( - String.format("Could not find node at %s; Is your cluster info up to date", nodeId)); + "Could not find node at %s; Is your cluster info up to date".formatted(nodeId)); } /** @@ -210,7 +210,7 @@ public RedisClusterNode lookup(RedisClusterNode node) { } throw new ClusterStateFailureException( - String.format("Could not find node at %s; Have you provided either host and port or the nodeId", node)); + ("Could not find node at %s;" + " Have you provided either host and port or the nodeId").formatted(node)); } /** diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java b/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java index 22827c1d2b..f03067e21f 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/ConnectionUtils.java b/src/main/java/org/springframework/data/redis/connection/ConnectionUtils.java index 9c768307f4..58af2c6ba1 100644 --- a/src/main/java/org/springframework/data/redis/connection/ConnectionUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/ConnectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/DataType.java b/src/main/java/org/springframework/data/redis/connection/DataType.java index 746eb09a9b..1b0635fbeb 100644 --- a/src/main/java/org/springframework/data/redis/connection/DataType.java +++ b/src/main/java/org/springframework/data/redis/connection/DataType.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/DecoratedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DecoratedRedisConnection.java index 2757810c0e..19ad271389 100644 --- a/src/main/java/org/springframework/data/redis/connection/DecoratedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DecoratedRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/DefaultMessage.java b/src/main/java/org/springframework/data/redis/connection/DefaultMessage.java index 0ec00955e9..b96a0895f3 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultMessage.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/DefaultSortParameters.java b/src/main/java/org/springframework/data/redis/connection/DefaultSortParameters.java index 44526bd999..bd50f34ba6 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultSortParameters.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultSortParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 80cc128e55..c2ea198d8b 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -34,10 +35,19 @@ import org.springframework.data.redis.connection.convert.ListConverter; import org.springframework.data.redis.connection.convert.MapConverter; import org.springframework.data.redis.connection.convert.SetConverter; -import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.connection.stream.ByteRecord; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.PendingMessagesSummary; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamReadOptions; +import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; @@ -348,13 +358,13 @@ public Long exists(byte[]... keys) { } @Override - public Boolean expire(byte[] key, long seconds) { - return convertAndReturn(delegate.expire(key, seconds), Converters.identityConverter()); + public Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition) { + return convertAndReturn(delegate.expire(key, seconds, condition), Converters.identityConverter()); } @Override - public Boolean expireAt(byte[] key, long unixTime) { - return convertAndReturn(delegate.expireAt(key, unixTime), Converters.identityConverter()); + public Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition) { + return convertAndReturn(delegate.expireAt(key, unixTime, condition), Converters.identityConverter()); } @Override @@ -770,6 +780,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op return convertAndReturn(delegate.set(key, value, expiration, option), Converters.identityConverter()); } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return convertAndReturn(delegate.setGet(key, value, expiration, option), Converters.identityConverter()); + } + @Override public Boolean setBit(byte[] key, long offset, boolean value) { return convertAndReturn(delegate.setBit(key, offset, value), Converters.identityConverter()); @@ -1292,13 +1307,13 @@ public Long zLexCount(String key, org.springframework.data.domain.Range } @Override - public Boolean pExpire(byte[] key, long millis) { - return convertAndReturn(delegate.pExpire(key, millis), Converters.identityConverter()); + public Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition) { + return convertAndReturn(delegate.pExpire(key, millis, condition), Converters.identityConverter()); } @Override - public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { - return convertAndReturn(delegate.pExpireAt(key, unixTimeInMillis), Converters.identityConverter()); + public Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition) { + return convertAndReturn(delegate.pExpireAt(key, unixTimeInMillis, condition), Converters.identityConverter()); } @Override @@ -1481,13 +1496,13 @@ public Boolean exists(String key) { } @Override - public Boolean expire(String key, long seconds) { - return expire(serialize(key), seconds); + public Boolean expire(String key, long seconds, ExpirationOptions.Condition condition) { + return expire(serialize(key), seconds, condition); } @Override - public Boolean expireAt(String key, long unixTime) { - return expireAt(serialize(key), unixTime); + public Boolean expireAt(String key, long unixTime, ExpirationOptions.Condition condition) { + return expireAt(serialize(key), unixTime, condition); } @Override @@ -2475,13 +2490,13 @@ public Object execute(String command, String... args) { } @Override - public Boolean pExpire(String key, long millis) { - return pExpire(serialize(key), millis); + public Boolean pExpire(String key, long millis, ExpirationOptions.Condition condition) { + return pExpire(serialize(key), millis, condition); } @Override - public Boolean pExpireAt(String key, long unixTimeInMillis) { - return pExpireAt(serialize(key), unixTimeInMillis); + public Boolean pExpireAt(String key, long unixTimeInMillis, ExpirationOptions.Condition condition) { + return pExpireAt(serialize(key), unixTimeInMillis, condition); } @Override @@ -2560,12 +2575,105 @@ public Cursor> hScan(byte[] key, ScanOptions options) { return this.delegate.hScan(key, options); } - @Nullable @Override public Long hStrLen(byte[] key, byte[] field) { return convertAndReturn(delegate.hStrLen(key, field), Converters.identityConverter()); } + public @Nullable List applyHashFieldExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, + ExpirationOptions options, byte[]... fields) { + return this.delegate.applyHashFieldExpiration(key, expiration, options, fields); + } + + @Override + public List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpire(key, seconds, condition, fields); + } + + @Override + public List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hpExpire(key, millis, condition, fields); + } + + @Override + public List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpireAt(key, unixTime, condition, fields); + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields) { + return this.delegate.hpExpireAt(key, unixTimeInMillis, condition, fields); + } + + @Override + public List hPersist(byte[] key, byte[]... fields) { + return this.delegate.hPersist(key, fields); + } + + @Override + public List hTtl(byte[] key, byte[]... fields) { + return this.delegate.hTtl(key, fields); + } + + @Override + public List hpTtl(byte[] key, byte[]... fields) { + return this.delegate.hpTtl(key, fields); + } + + @Override + public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + return this.delegate.hTtl(key, timeUnit, fields); + } + + public @Nullable List applyExpiration(String key, + org.springframework.data.redis.core.types.Expiration expiration, + ExpirationOptions options, String... fields) { + return this.applyHashFieldExpiration(serialize(key), expiration, options, serializeMulti(fields)); + } + + @Override + public List hExpire(String key, long seconds, ExpirationOptions.Condition condition, String... fields) { + return hExpire(serialize(key), seconds, condition, serializeMulti(fields)); + } + + @Override + public List hpExpire(String key, long millis, ExpirationOptions.Condition condition, String... fields) { + return hpExpire(serialize(key), millis, condition, serializeMulti(fields)); + } + + @Override + public List hExpireAt(String key, long unixTime, ExpirationOptions.Condition condition, String... fields) { + return hExpireAt(serialize(key), unixTime, condition, serializeMulti(fields)); + } + + @Override + public List hpExpireAt(String key, long unixTimeInMillis, ExpirationOptions.Condition condition, + String... fields) { + return hpExpireAt(serialize(key), unixTimeInMillis, condition, serializeMulti(fields)); + } + + @Override + public List hPersist(String key, String... fields) { + return hPersist(serialize(key), serializeMulti(fields)); + } + + @Override + public List hTtl(String key, String... fields) { + return hTtl(serialize(key), serializeMulti(fields)); + } + + @Override + public List hTtl(String key, TimeUnit timeUnit, String... fields) { + return hTtl(serialize(key), timeUnit, serializeMulti(fields)); + } + + @Override + public List hpTtl(String key, String... fields) { + return hTtl(serialize(key), serializeMulti(fields)); + } + @Override public void setClientName(byte[] name) { this.delegate.setClientName(name); diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringTuple.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringTuple.java index 7851a0f2a8..b35522c642 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringTuple.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringTuple.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/DefaultedRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java index dfa5986df5..369a272476 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index aaeaafe18b..f3be7ab276 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -65,6 +65,7 @@ * @author ihaohong * @author Dennis Neufeld * @author Shyngys Sapraliyev + * @author Tihomir Mateev * @since 2.0 */ @Deprecated @@ -160,7 +161,14 @@ default Boolean renameNX(byte[] sourceKey, byte[] targetKey) { @Override @Deprecated default Boolean expire(byte[] key, long seconds) { - return keyCommands().expire(key, seconds); + return keyCommands().expire(key, seconds, ExpirationOptions.Condition.ALWAYS); + } + + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ + @Override + @Deprecated + default Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition) { + return keyCommands().expire(key, seconds, condition); } /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @@ -202,21 +210,42 @@ default Long pTtl(byte[] key, TimeUnit timeUnit) { @Override @Deprecated default Boolean pExpire(byte[] key, long millis) { - return keyCommands().pExpire(key, millis); + return keyCommands().pExpire(key, millis, ExpirationOptions.Condition.ALWAYS); + } + + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ + @Override + @Deprecated + default Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition) { + return keyCommands().pExpire(key, millis, condition); } /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @Override @Deprecated default Boolean pExpireAt(byte[] key, long unixTimeInMillis) { - return keyCommands().pExpireAt(key, unixTimeInMillis); + return keyCommands().pExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS); + } + + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ + @Override + @Deprecated + default Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition) { + return keyCommands().pExpireAt(key, unixTimeInMillis, condition); } /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @Override @Deprecated default Boolean expireAt(byte[] key, long unixTime) { - return keyCommands().expireAt(key, unixTime); + return keyCommands().expireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS); + } + + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ + @Override + @Deprecated + default Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition) { + return keyCommands().expireAt(key, unixTime, condition); } /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @@ -326,6 +355,13 @@ default Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption o return stringCommands().set(key, value, expiration, option); } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return stringCommands().setGet(key, value, expiration, option); + } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ @Override @Deprecated @@ -1470,6 +1506,101 @@ default Long hStrLen(byte[] key, byte[] field) { return hashCommands().hStrLen(key, field); } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpire(byte[] key, long seconds, byte[]... fields) { + return hashCommands().hExpire(key, seconds, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hExpire(key, seconds, condition, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpire(byte[] key, long millis, byte[]... fields) { + return hashCommands().hpExpire(key, millis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hpExpire(key, millis, condition, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { + return hashCommands().hExpireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hExpireAt(key, unixTime, condition, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + return hashCommands().hpExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hpExpireAt(key, unixTimeInMillis, condition, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hPersist(byte[] key, byte[]... fields) { + return hashCommands().hPersist(key, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hTtl(byte[] key, byte[]... fields) { + return hashCommands().hTtl(key, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + return hashCommands().hTtl(key, timeUnit, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpTtl(byte[] key, byte[]... fields) { + return hashCommands().hpTtl(key, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default @Nullable List applyHashFieldExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, ExpirationOptions options, + byte[]... fields) { + return hashCommands().applyHashFieldExpiration(key, expiration, options, fields); + } + // GEO COMMANDS /** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */ @@ -1841,9 +1972,8 @@ default T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated - default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, - org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByLex(dstKey, srcKey, range, limit); } @@ -1860,7 +1990,7 @@ default Long zRangeStoreRevByLex(byte[] dstKey, byte[] srcKey, org.springframewo @Deprecated default Long zRangeStoreByScore(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByScore(dstKey, srcKey, range, limit); } diff --git a/src/main/java/org/springframework/data/redis/connection/ExpirationOptions.java b/src/main/java/org/springframework/data/redis/connection/ExpirationOptions.java new file mode 100644 index 0000000000..900f27bd9d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ExpirationOptions.java @@ -0,0 +1,157 @@ +/* + * Copyright 2025 the original author 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.redis.connection; + +import java.util.Objects; + +import org.springframework.lang.Contract; +import org.springframework.util.ObjectUtils; + +/** + * Expiration options for Expiation updates. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.5 + */ +public class ExpirationOptions { + + private static final ExpirationOptions NONE = new ExpirationOptions(Condition.ALWAYS); + private final Condition condition; + + ExpirationOptions(Condition condition) { + this.condition = condition; + } + + /** + * @return an empty expiration options object. + */ + public static ExpirationOptions none() { + return NONE; + } + + /** + * @return builder for creating {@code FieldExpireOptionsBuilder}. + */ + public static ExpirationOptionsBuilder builder() { + return new ExpirationOptionsBuilder(); + } + + public Condition getCondition() { + return condition; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExpirationOptions that = (ExpirationOptions) o; + return ObjectUtils.nullSafeEquals(this.condition, that.condition); + } + + @Override + public int hashCode() { + return Objects.hash(condition); + } + + /** + * Builder to build {@link ExpirationOptions} + */ + public static class ExpirationOptionsBuilder { + + private Condition condition = Condition.ALWAYS; + + private ExpirationOptionsBuilder() {} + + /** + * Apply to fields that have no expiration. + */ + @Contract("-> this") + public ExpirationOptionsBuilder nx() { + this.condition = Condition.NX; + return this; + } + + /** + * Apply to fields that have an existing expiration. + */ + @Contract("-> this") + public ExpirationOptionsBuilder xx() { + this.condition = Condition.XX; + return this; + } + + /** + * Apply to fields when the new expiration is greater than the current one. + */ + @Contract("-> this") + public ExpirationOptionsBuilder gt() { + this.condition = Condition.GT; + return this; + } + + /** + * Apply to fields when the new expiration is lower than the current one. + */ + @Contract("-> this") + public ExpirationOptionsBuilder lt() { + this.condition = Condition.LT; + return this; + } + + public ExpirationOptions build() { + return condition == Condition.ALWAYS ? NONE : new ExpirationOptions(condition); + } + + } + + /** + * Conditions to apply when changing expiration. + */ + public enum Condition { + + /** + * Always apply expiration. + */ + ALWAYS, + + /** + * Set expiration only when the field has no expiration. + */ + NX, + + /** + * Set expiration only when the field has an existing expiration. + */ + XX, + + /** + * Set expiration only when the new expiration is greater than current one. + */ + GT, + + /** + * Set expiration only when the new expiration is greater than current one. + */ + LT + + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/FutureResult.java b/src/main/java/org/springframework/data/redis/connection/FutureResult.java index 122e889798..58da880234 100644 --- a/src/main/java/org/springframework/data/redis/connection/FutureResult.java +++ b/src/main/java/org/springframework/data/redis/connection/FutureResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/Limit.java b/src/main/java/org/springframework/data/redis/connection/Limit.java index f47e2b6747..d6abf62b85 100644 --- a/src/main/java/org/springframework/data/redis/connection/Limit.java +++ b/src/main/java/org/springframework/data/redis/connection/Limit.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/Message.java b/src/main/java/org/springframework/data/redis/connection/Message.java index 5be91f3897..138f61f3b2 100644 --- a/src/main/java/org/springframework/data/redis/connection/Message.java +++ b/src/main/java/org/springframework/data/redis/connection/Message.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/MessageListener.java b/src/main/java/org/springframework/data/redis/connection/MessageListener.java index fcf894a413..4857b45c6d 100644 --- a/src/main/java/org/springframework/data/redis/connection/MessageListener.java +++ b/src/main/java/org/springframework/data/redis/connection/MessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/NamedNode.java b/src/main/java/org/springframework/data/redis/connection/NamedNode.java index a638ed5781..5ccf100762 100644 --- a/src/main/java/org/springframework/data/redis/connection/NamedNode.java +++ b/src/main/java/org/springframework/data/redis/connection/NamedNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/PoolException.java b/src/main/java/org/springframework/data/redis/connection/PoolException.java index 06969e9ace..1dc6b89cbb 100644 --- a/src/main/java/org/springframework/data/redis/connection/PoolException.java +++ b/src/main/java/org/springframework/data/redis/connection/PoolException.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/ReactiveClusterCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterCommands.java index da4fad9d76..d780acced3 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/ReactiveClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java index d4496375cd..ae97652ba0 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java index 60e2390988..6674520b33 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java index 03df631960..2623eeda84 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java index 21aea13ba1..d650b738eb 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java index ad5bf5a3dc..f331728178 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java index 5968745c1c..3840a33b32 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterScriptingCommands.java index b027421546..a7e0f171c7 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/ReactiveClusterServerCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterServerCommands.java index 050715b734..2a19d5fcfe 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/ReactiveClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java index 8590edeab9..b20809fd24 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStreamCommands.java index 696958fd77..d2dacc7fad 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/ReactiveClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java index 3800aec877..a410627a3a 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java index f11a90d7cb..167d0f59b1 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java index cbeec12b28..80c9fde257 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index 063dd6e285..385b652f2b 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,11 +19,14 @@ import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.reactivestreams.Publisher; @@ -36,6 +39,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -44,10 +48,34 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Tihomir Mateev * @since 2.0 */ public interface ReactiveHashCommands { + /** + * {@link Command} for hash-bound operations. + * + * @author Christoph Strobl + * @author Tihomir Mateev + */ + class HashFieldsCommand extends KeyCommand { + + private final List fields; + + private HashFieldsCommand(@Nullable ByteBuffer key, List fields) { + super(key); + this.fields = fields; + } + + /** + * @return never {@literal null}. + */ + public List getFields() { + return fields; + } + } + /** * {@literal HSET} {@link Command}. * @@ -216,15 +244,10 @@ default Mono hMSet(ByteBuffer key, Map fieldVal * @author Christoph Strobl * @see Redis Documentation: HGET */ - class HGetCommand extends KeyCommand { - - private List fields; + class HGetCommand extends HashFieldsCommand { private HGetCommand(@Nullable ByteBuffer key, List fields) { - - super(key); - - this.fields = fields; + super(key, fields); } /** @@ -263,14 +286,7 @@ public HGetCommand from(ByteBuffer key) { Assert.notNull(key, "Key must not be null"); - return new HGetCommand(key, fields); - } - - /** - * @return never {@literal null}. - */ - public List getFields() { - return fields; + return new HGetCommand(key, getFields()); } } @@ -289,7 +305,7 @@ default Mono hGet(ByteBuffer key, ByteBuffer field) { /** * Get values for given {@literal fields} from hash at {@literal key}. Values are in the order of the requested keys. - * Absent field values are represented using {@code null} in the resulting {@link List}. + * Absent field values are represented using {@literal null} in the resulting {@link List}. * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. @@ -306,7 +322,7 @@ default Mono> hMGet(ByteBuffer key, Collection fiel /** * Get values for given {@literal fields} from hash at {@literal key}. Values are in the order of the requested keys. - * Absent field values are represented using {@code null} in the resulting {@link List}. + * Absent field values are represented using {@literal null} in the resulting {@link List}. * * @param commands must not be {@literal null}. * @return @@ -394,15 +410,10 @@ default Mono hExists(ByteBuffer key, ByteBuffer field) { * @author Christoph Strobl * @see Redis Documentation: HDEL */ - class HDelCommand extends KeyCommand { - - private final List fields; + class HDelCommand extends HashFieldsCommand { private HDelCommand(@Nullable ByteBuffer key, List fields) { - - super(key); - - this.fields = fields; + super(key, fields); } /** @@ -441,14 +452,7 @@ public HDelCommand from(ByteBuffer key) { Assert.notNull(key, "Key must not be null"); - return new HDelCommand(key, fields); - } - - /** - * @return never {@literal null}. - */ - public List getFields() { - return fields; + return new HDelCommand(key, getFields()); } } @@ -842,4 +846,413 @@ default Mono hStrLen(ByteBuffer key, ByteBuffer field) { * @since 2.1 */ Flux> hStrLen(Publisher commands); + + /** + * @since 3.5 + */ + class HashExpireCommand extends HashFieldsCommand { + + private final Expiration expiration; + private final ExpirationOptions options; + + private HashExpireCommand(@Nullable ByteBuffer key, List fields, Expiration expiration, + ExpirationOptions options) { + + super(key, fields); + + this.expiration = expiration; + this.options = options; + } + + /** + * Creates a new {@link HashExpireCommand}. + * + * @param fields the {@code field} names to apply expiration to + * @param timeout the actual timeout + * @param unit the unit of measure for the {@code timeout}. + * @return new instance of {@link HashExpireCommand}. + */ + public static HashExpireCommand expire(List fields, long timeout, TimeUnit unit) { + + Assert.notNull(fields, "Field must not be null"); + return expire(fields, Expiration.from(timeout, unit)); + } + + /** + * Creates a new {@link HashExpireCommand}. + * + * @param fields the {@code field} names to apply expiration to. + * @param ttl the actual timeout. + * @return new instance of {@link HashExpireCommand}. + */ + public static HashExpireCommand expire(List fields, Duration ttl) { + + Assert.notNull(fields, "Field must not be null"); + return expire(fields, Expiration.from(ttl)); + } + + /** + * Creates a new {@link HashExpireCommand}. + * + * @param fields the {@code field} names to apply expiration to + * @param expiration the {@link Expiration} to apply to the given {@literal fields}. + * @return new instance of {@link HashExpireCommand}. + */ + public static HashExpireCommand expire(List fields, Expiration expiration) { + return new HashExpireCommand(null, fields, expiration, ExpirationOptions.none()); + } + + /** + * Creates a new {@link HashExpireCommand}. + * + * @param fields the {@code field} names to apply expiration to + * @param ttl the unix point in time when to expire the given {@literal fields}. + * @param precision can be {@link TimeUnit#SECONDS} or {@link TimeUnit#MILLISECONDS}. + * @return new instance of {@link HashExpireCommand}. + */ + public static HashExpireCommand expireAt(List fields, Instant ttl, TimeUnit precision) { + + if (precision.compareTo(TimeUnit.MILLISECONDS) > 0) { + return expire(fields, Expiration.unixTimestamp(ttl.getEpochSecond(), TimeUnit.SECONDS)); + } + + return expire(fields, Expiration.unixTimestamp(ttl.toEpochMilli(), TimeUnit.MILLISECONDS)); + } + + /** + * @param key the {@literal key} from which to expire the {@literal fields} from. + * @return new instance of {@link HashExpireCommand}. + */ + public HashExpireCommand from(ByteBuffer key) { + return new HashExpireCommand(key, getFields(), expiration, options); + } + + /** + * @param options additional options to be sent along with the command. + * @return new instance of {@link HashExpireCommand}. + */ + public HashExpireCommand withOptions(ExpirationOptions options) { + return new HashExpireCommand(getKey(), getFields(), getExpiration(), options); + } + + public Expiration getExpiration() { + return expiration; + } + + public ExpirationOptions getOptions() { + return options; + } + } + + /** + * Expire a given {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param duration must not be {@literal null}. + * @return a {@link Mono} emitting the expiration result - {@code 2} indicating the specific field is deleted already + * due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; + * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); + * {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + default Mono hExpire(ByteBuffer key, Duration duration, ByteBuffer field) { + + Assert.notNull(duration, "Duration must not be null"); + + return hExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has + * passed. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param duration must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + default Flux hExpire(ByteBuffer key, Duration duration, List fields) { + + Assert.notNull(duration, "Duration must not be null"); + + return applyHashFieldExpiration(Flux.just(HashExpireCommand.expire(fields, duration).from(key))) + .mapNotNull(NumericResponse::getOutput); + } + + /** + * Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has + * passed. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; + * @since 3.5 + * @see Redis Documentation: HEXPIRE + */ + Flux> applyHashFieldExpiration(Publisher commands); + + /** + * Expire a given {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param duration must not be {@literal null}. + * @return a {@link Mono} emitting the expiration result - {@code 2} indicating the specific field is deleted already + * due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; + * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); + * {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + default Mono hpExpire(ByteBuffer key, Duration duration, ByteBuffer field) { + + Assert.notNull(duration, "Duration must not be null"); + + return hpExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has + * passed. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param duration must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + default Flux hpExpire(ByteBuffer key, Duration duration, List fields) { + + Assert.notNull(duration, "Duration must not be null"); + + return applyHashFieldExpiration(Flux.just(new HashExpireCommand(key, fields, + Expiration.from(duration.toMillis(), TimeUnit.MILLISECONDS), ExpirationOptions.none()))) + .mapNotNull(NumericResponse::getOutput); + } + + /** + * Expire a given {@literal field} in a given {@link Instant} of time, indicated as an absolute + * Unix timestamp in seconds since Unix epoch + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return a {@link Mono} emitting the expiration result - {@code 2} indicating the specific field is deleted already + * due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is + * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is + * not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + default Mono hExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { + + Assert.notNull(expireAt, "Duration must not be null"); + + return hExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Expire a {@link List} of {@literal field} in a given {@link Instant} of time, indicated as an absolute + * Unix timestamp in seconds since Unix epoch + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + default Flux hExpireAt(ByteBuffer key, Instant expireAt, List fields) { + + Assert.notNull(expireAt, "Duration must not be null"); + + return applyHashFieldExpiration(Flux.just(HashExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key))) + .mapNotNull(NumericResponse::getOutput); + } + + /** + * Expire a given {@literal field} in a given {@link Instant} of time, indicated as an absolute + * Unix timestamp in milliseconds since Unix epoch + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return a {@link Mono} emitting the expiration result - {@code 2} indicating the specific field is deleted already + * due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is + * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is + * not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + default Mono hpExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { + + Assert.notNull(expireAt, "Duration must not be null"); + + return hpExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Expire a {@link List} of {@literal field} in a given {@link Instant} of time, indicated as an absolute + * Unix timestamp in milliseconds since Unix epoch + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + default Flux hpExpireAt(ByteBuffer key, Instant expireAt, List fields) { + + Assert.notNull(expireAt, "Duration must not be null"); + + return applyHashFieldExpiration( + Flux.just(HashExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key))) + .mapNotNull(NumericResponse::getOutput); + } + + /** + * Persist a given {@literal field} removing any associated expiration, measured as absolute + * Unix timestamp in seconds since Unix epoch + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return a {@link Mono} emitting the persist result - {@code 1} indicating expiration time is removed; {@code -1} + * field has no expiration time to be removed; {@code -2} indicating there is no such field; + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + default Mono hPersist(ByteBuffer key, ByteBuffer field) { + return hPersist(key, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Persist a given {@link List} of {@literal field} removing any associated expiration. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a {@link Flux} emitting the persisting results one by one - {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + default Flux hPersist(ByteBuffer key, List fields) { + return hPersist(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); + } + + /** + * Persist a given {@link List} of {@literal field} removing any associated expiration. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the persisting results one by one - {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; * @since 3.5 + * @see Redis Documentation: HPERSIST + */ + Flux> hPersist(Publisher commands); + + /** + * Returns the time-to-live of a given {@literal field} in seconds. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return a {@link Mono} emitting the TTL result - the time to live in seconds; or a negative value to signal an + * error. The command returns {@code -1} if the key exists but has no associated expiration time. The command + * returns {@code -2} if the key does not exist; + * @see Redis Documentation: HTTL + * @since 3.5 + */ + default Mono hTtl(ByteBuffer key, ByteBuffer field) { + return hTtl(key, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Returns the time-to-live of all the given {@literal field} in the {@link List} in seconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a {@link Flux} emitting the TTL results one by one - the time to live in seconds; or a negative value to + * signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. + * The command returns {@code -2} if the key does not exist; + * @see Redis Documentation: HTTL + * @since 3.5 + */ + default Flux hTtl(ByteBuffer key, List fields) { + return hTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); + } + + /** + * Returns the time-to-live of all the given {@literal field} in the {@link List} in seconds. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the persisting results one by one - the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; + * @since 3.5 + * @see Redis Documentation: HTTL + */ + Flux> hTtl(Publisher commands); + + /** + * Returns the time-to-live of a given {@literal field} in milliseconds. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return a {@link Mono} emitting the TTL result - the time to live in milliseconds; or a negative value to signal an + * error. The command returns {@code -1} if the key exists but has no associated expiration time. The command + * returns {@code -2} if the key does not exist; + * @see Redis Documentation: HPTTL + * @since 3.5 + */ + default Mono hpTtl(ByteBuffer key, ByteBuffer field) { + return hpTtl(key, Collections.singletonList(field)).singleOrEmpty(); + } + + /** + * Returns the time-to-live of all the given {@literal field} in the {@link List} in milliseconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a {@link Flux} emitting the TTL results one by one - the time to live in milliseconds; or a negative value + * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. + * The command returns {@code -2} if the key does not exist; + * @see Redis Documentation: HPTTL + * @since 3.5 + */ + default Flux hpTtl(ByteBuffer key, List fields) { + return hpTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); + } + + /** + * Returns the time-to-live of all the given {@literal field} in the {@link List} in milliseconds. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the persisting results one by one - the time to live in milliseconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; + * @since 3.5 + * @see Redis Documentation: HPTTL + */ + Flux> hpTtl(Publisher commands); + } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java index d10975851a..cba1cc8fac 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index bd3f4d73dd..3354cf9af1 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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.util.List; import org.reactivestreams.Publisher; + import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; @@ -32,6 +33,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.core.KeyScanOptions; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -40,6 +42,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Dahye Anne Lee * @since 2.0 */ public interface ReactiveKeyCommands { @@ -176,6 +179,16 @@ default Mono exists(ByteBuffer key) { return exists(Mono.just(new KeyCommand(key))).next().map(BooleanResponse::getOutput); } + /** + * Determine the number of given {@literal keys} that exist. + * + * @param keys must not be {@literal null} or {@literal empty}. + * @return {@link Mono} emitting {@literal the number of existing keys}. + * @see Redis Documentation: EXISTS + * @since 3.5 + */ + Mono exists(List keys); + /** * Determine if given {@literal key} exists. * @@ -513,13 +526,35 @@ default Mono mUnlink(List keys) { */ class ExpireCommand extends KeyCommand { - private @Nullable Duration timeout; + private final Expiration expiration; + private final ExpirationOptions options; + + private ExpireCommand(ByteBuffer key, Duration timeout) { + this(key, Expiration.from(timeout), ExpirationOptions.none()); + } - private ExpireCommand(ByteBuffer key, @Nullable Duration timeout) { + private ExpireCommand(@Nullable ByteBuffer key, Expiration expiration, ExpirationOptions options) { super(key); - this.timeout = timeout; + this.expiration = expiration; + this.options = options; + } + + /** + * Creates a new {@link ExpireCommand} given a {@link ByteBuffer key} and {@link Expiration}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @return a new {@link ExpireCommand} for {@link ByteBuffer key} and {@link Expiration}. + * @since 3.5 + */ + public static ExpireCommand expire(ByteBuffer key, Expiration expiration) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + + return new ExpireCommand(key, expiration, ExpirationOptions.none()); } /** @@ -532,7 +567,7 @@ public static ExpireCommand key(ByteBuffer key) { Assert.notNull(key, "Key must not be null"); - return new ExpireCommand(key, null); + return new ExpireCommand(key, Expiration.persistent(), ExpirationOptions.none()); } /** @@ -545,7 +580,21 @@ public ExpireCommand timeout(Duration timeout) { Assert.notNull(timeout, "Timeout must not be null"); - return new ExpireCommand(getKey(), timeout); + return new ExpireCommand(getKey(), Expiration.from(timeout), options); + } + + /** + * Applies the {@literal timeout}. Constructs a new command instance with all previously configured properties. + * + * @param timeout must not be {@literal null}. + * @return a new {@link ExpireCommand} with {@literal timeout} applied. + * @since 3.5 + */ + public ExpireCommand expire(Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null"); + + return new ExpireCommand(getKey(), Expiration.from(timeout), options); } /** @@ -553,10 +602,50 @@ public ExpireCommand timeout(Duration timeout) { */ @Nullable public Duration getTimeout() { - return timeout; + + if (expiration.isUnixTimestamp() || expiration.isPersistent()) { + return null; + } + + return Duration.ofMillis(expiration.getExpirationTimeInMilliseconds()); } + + /** + * @param options additional options to be sent along with the command. + * @return new instance of {@link ExpireCommand}. + * @since 3.5 + */ + public ExpireCommand withOptions(ExpirationOptions options) { + return new ExpireCommand(getKey(), getExpiration(), options); + } + + public Expiration getExpiration() { + return expiration; + } + + public ExpirationOptions getOptions() { + return options; + } + } + /** + * Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has + * passed. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the expiration results one by one, {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @since 3.5 + * @see Redis Documentation: EXPIRE + * @see Redis Documentation: PEXPIRE + * @see Redis Documentation: EXPIREAT + * @see Redis Documentation: PEXPIREAT + * @see Redis Documentation: PERSIST + */ + Flux> applyExpiration(Publisher commands); + /** * Set time to live for given {@code key} in seconds. * @@ -581,7 +670,9 @@ default Mono expire(ByteBuffer key, Duration timeout) { * result. * @see Redis Documentation: EXPIRE */ - Flux> expire(Publisher commands); + default Flux> expire(Publisher commands) { + return applyExpiration(commands); + } /** * Set time to live for given {@code key} in milliseconds. @@ -607,7 +698,9 @@ default Mono pExpire(ByteBuffer key, Duration timeout) { * result. * @see Redis Documentation: PEXPIRE */ - Flux> pExpire(Publisher commands); + default Flux> pExpire(Publisher commands) { + return applyExpiration(commands); + } /** * {@code EXPIREAT}/{@code PEXPIREAT} command parameters. @@ -619,12 +712,18 @@ default Mono pExpire(ByteBuffer key, Duration timeout) { class ExpireAtCommand extends KeyCommand { private @Nullable Instant expireAt; + private final ExpirationOptions options; + + private ExpireAtCommand(ByteBuffer key, Instant expireAt) { + this(key, expireAt, ExpirationOptions.none()); + } - private ExpireAtCommand(ByteBuffer key, @Nullable Instant expireAt) { + private ExpireAtCommand(@Nullable ByteBuffer key, Instant expireAt, ExpirationOptions options) { super(key); this.expireAt = expireAt; + this.options = options; } /** @@ -653,6 +752,15 @@ public ExpireAtCommand timeout(Instant expireAt) { return new ExpireAtCommand(getKey(), expireAt); } + /** + * @param options additional options to be sent along with the command. + * @return new instance of {@link ExpireAtCommand}. + * @since 3.5 + */ + public ExpireAtCommand withOptions(ExpirationOptions options) { + return new ExpireAtCommand(getKey(), getExpireAt(), options); + } + /** * @return can be {@literal null}. */ @@ -660,6 +768,11 @@ public ExpireAtCommand timeout(Instant expireAt) { public Instant getExpireAt() { return expireAt; } + + public ExpirationOptions getOptions() { + return options; + } + } /** diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java index f3fbd20f3e..d92a99a270 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java index bbade90af8..31f80cc460 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactivePubSubCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactivePubSubCommands.java index d07155b4ab..5ea9d74885 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactivePubSubCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactivePubSubCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/ReactiveRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java index 2dc4efd081..cec6b7991b 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java index 7da6929ad3..5dfb6d9db8 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveRedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java index 05e726e753..3492913549 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/ReactiveScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveScriptingCommands.java index 5337763728..4ad5d59004 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/ReactiveServerCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveServerCommands.java index d95b718503..c0d65c1a81 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/ReactiveSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java index 845301dba9..76911fd441 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/ReactiveStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStreamCommands.java index 460a8cc3fe..2860dc691c 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -32,6 +32,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.RedisStreamCommands.XPendingOptions; import org.springframework.data.redis.connection.stream.ByteBufferRecord; import org.springframework.data.redis.connection.stream.Consumer; @@ -58,6 +59,7 @@ * @author Tugdual Grall * @author Dengliming * @author Mark John Moreno + * @author jinkshower * @since 2.2 */ public interface ReactiveStreamCommands { @@ -335,7 +337,7 @@ public Long getMaxlen() { * @since 2.3 */ public boolean hasMaxlen() { - return maxlen != null && maxlen > 0; + return maxlen != null; } /** @@ -394,11 +396,40 @@ default Mono xAdd(ByteBufferRecord record) { return xAdd(Mono.just(AddStreamRecord.of(record))).next().map(CommandResponse::getOutput); } + /** + * Add stream record with the specified options. + * + * @param record must not be {@literal null}. + * @param xAddOptions parameters for the {@literal XADD} call. Must not be {@literal null}. + * @return {@link Mono} the {@link RecordId id}. + * @see Redis Documentation: XADD + * @since 3.4 + */ + default Mono xAdd(ByteBufferRecord record, XAddOptions xAddOptions) { + + Assert.notNull(record, "Record must not be null"); + Assert.notNull(xAddOptions, "XAddOptions must not be null"); + + AddStreamRecord addStreamRecord = AddStreamRecord.of(record) + .approximateTrimming(xAddOptions.isApproximateTrimming()) + .makeNoStream(xAddOptions.isNoMkStream()); + + if (xAddOptions.hasMaxlen()) { + addStreamRecord = addStreamRecord.maxlen(xAddOptions.getMaxlen()); + } + + if (xAddOptions.hasMinId()) { + addStreamRecord = addStreamRecord.minId(xAddOptions.getMinId()); + } + + return xAdd(Mono.just(addStreamRecord)).next().map(CommandResponse::getOutput); + } + /** * Add stream record with given {@literal body} to {@literal key}. * * @param commands must not be {@literal null}. - * @return {@link Flux} emitting the {@link RecordId} on by for for the given {@link AddStreamRecord} commands. + * @return {@link Flux} emitting the {@link RecordId} on by for the given {@link AddStreamRecord} commands. * @see Redis Documentation: XADD */ Flux> xAdd(Publisher commands); @@ -654,7 +685,7 @@ default Mono xPending(ByteBuffer key, String groupName) Assert.notNull(key, "Key must not be null"); Assert.notNull(groupName, "GroupName must not be null"); - return xPendingSummary(Mono.just(new PendingRecordsCommand(key, groupName, null, Range.unbounded(), null))).next() + return xPendingSummary(Mono.just(PendingRecordsCommand.pending(key, groupName))).next() .map(CommandResponse::getOutput); } @@ -695,7 +726,7 @@ default Mono xPending(ByteBuffer key, Consumer consumer) { */ @Nullable default Mono xPending(ByteBuffer key, String groupName, String consumerName) { - return xPending(Mono.just(new PendingRecordsCommand(key, groupName, consumerName, Range.unbounded(), null))).next() + return xPending(Mono.just(PendingRecordsCommand.pending(key, groupName).consumer(consumerName))).next() .map(CommandResponse::getOutput); } @@ -712,7 +743,7 @@ default Mono xPending(ByteBuffer key, String groupName, String * @since 2.3 */ default Mono xPending(ByteBuffer key, String groupName, Range range, Long count) { - return xPending(Mono.just(new PendingRecordsCommand(key, groupName, null, range, count))).next() + return xPending(Mono.just(PendingRecordsCommand.pending(key, groupName).range(range, count))).next() .map(CommandResponse::getOutput); } @@ -748,8 +779,8 @@ default Mono xPending(ByteBuffer key, Consumer consumer, Range< */ default Mono xPending(ByteBuffer key, String groupName, String consumerName, Range range, Long count) { - return xPending(Mono.just(new PendingRecordsCommand(key, groupName, consumerName, range, count))).next() - .map(CommandResponse::getOutput); + return xPending(Mono.just(PendingRecordsCommand.pending(key, groupName).consumer(consumerName).range(range, count))) + .next().map(CommandResponse::getOutput); } /** @@ -801,9 +832,15 @@ static PendingRecordsCommand pending(ByteBuffer key, String groupName) { /** * Create new {@link PendingRecordsCommand} with given {@link Range} and limit. * + * @param range must not be {@literal null}. + * @param count the max number of messages to return. Must not be negative. * @return new instance of {@link XPendingOptions}. */ - public PendingRecordsCommand range(Range range, Long count) { + public PendingRecordsCommand range(Range range, Long count) { + + Assert.notNull(range, "Range must not be null"); + Assert.isTrue(count > -1, "Count must not be negative"); + return new PendingRecordsCommand(getKey(), groupName, consumerName, range, count); } @@ -855,7 +892,7 @@ public boolean hasConsumer() { * @return {@literal true} count is set. */ public boolean isLimited() { - return count != null && count > -1; + return count != null; } } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java index 493b055dae..3c1bfc8eea 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Marcin Grzejszczak * @since 2.0 */ public interface ReactiveStringCommands { @@ -193,6 +194,41 @@ default Mono set(ByteBuffer key, ByteBuffer value, Expiration expiratio */ Flux> set(Publisher commands); + /** + * Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. Return the old string + * stored at key, or empty if key did not exist. An error is returned and SET aborted if the value stored at key is + * not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} for no expiration time or + * {@link Expiration#keepTtl()} to keep the existing. + * @param option must not be {@literal null}. + * @return + * @see Redis Documentation: SET + * @since 3.5 + */ + @Nullable + default Mono setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return setGet(Mono.just(SetCommand.set(key).value(value).withSetOption(option).expiring(expiration))).next() + .map(CommandResponse::getOutput); + } + + /** + * Set each and every item separately by invoking {@link SetCommand}. Return the old string stored at key, or empty if + * key did not exist. An error is returned and SET aborted if the value stored at key is not a string. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link ByteBufferResponse} holding the {@link SetCommand} along with the command result. + * @see Redis Documentation: SET + * @since 3.5 + */ + Flux> setGet(Publisher commands); + /** * Get single element stored at {@literal key}. * @@ -350,7 +386,7 @@ default Mono getSet(ByteBuffer key, ByteBuffer value) { /** * Get multiple values in one batch. Values are in the order of the requested keys. Absent field values are - * represented using {@code null} in the resulting {@link List}. + * represented using {@literal null} in the resulting {@link List}. * * @param keys must not be {@literal null}. * @return @@ -365,7 +401,7 @@ default Mono> mGet(List keys) { /** * Get multiple values at for {@literal keysets} in batches. Values are in the order of the requested keys. Absent - * field values are represented using {@code null} in the resulting {@link List}. + * field values are represented using {@literal null} in the resulting {@link List}. * * @param keysets must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveSubscription.java b/src/main/java/org/springframework/data/redis/connection/ReactiveSubscription.java index 2929544c97..153d139e8f 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveSubscription.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveSubscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/ReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java index 2abbb212ce..c4037e2c55 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1974,7 +1974,7 @@ default Mono zCard(ByteBuffer key) { } /** - * Get the size of sorted set with {@linByteBuffer keyCommand#getKey()}. + * Get the size of sorted set with {@link ByteBuffer keyCommand#getKey()}. * * @param commands must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java index 9a47dd7413..018a78b6cb 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/RedisClusterCommandsProvider.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommandsProvider.java index 7427559004..e26842e779 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterCommandsProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommandsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java index 620e132d02..8960d32ae1 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,6 @@ import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -63,11 +62,9 @@ public RedisClusterConfiguration() {} /** * Creates a new {@link RedisClusterConfiguration} for given {@link String hostPort} combinations. * - *

-	 * 
+	 * 
 	 * clusterHostAndPorts[0] = 127.0.0.1:23679
 	 * clusterHostAndPorts[1] = 127.0.0.1:23680 ...
-	 * 
 	 * 
* * @param clusterNodes must not be {@literal null}. @@ -161,7 +158,10 @@ public Set getClusterNodes() { * @param node must not be {@literal null}. */ public void addClusterNode(RedisNode node) { - this.clusterNodes.add(RedisAssertions.requireNonNull(node, "ClusterNode must not be null")); + + Assert.notNull(node, "ClusterNode must not be null"); + + this.clusterNodes.add(node); } /** @@ -211,7 +211,10 @@ public String getUsername() { @Override public void setPassword(RedisPassword password) { - this.password = RedisAssertions.requireNonNull(password, "RedisPassword must not be null"); + + Assert.notNull(password, "RedisPassword must not be null"); + + this.password = password; } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java index d079fae2f3..585780ecb1 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/RedisClusterNode.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java index 7f84a2110d..7a6443d484 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,6 @@ import java.util.LinkedHashSet; import java.util.Set; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -76,7 +75,9 @@ public RedisClusterNode(String id) { this(SlotRange.empty()); - this.id = RedisAssertions.requireNonNull(id, "Id must not be null"); + Assert.notNull(id, "Id must not be null"); + + this.id = id; } /** @@ -86,8 +87,10 @@ public RedisClusterNode(String id) { */ public RedisClusterNode(SlotRange slotRange) { + Assert.notNull(slotRange, "SlotRange must not be null"); + this.flags = Collections.emptySet(); - this.slotRange = RedisAssertions.requireNonNull(slotRange,"SlotRange must not be null"); + this.slotRange = slotRange; } /** @@ -101,8 +104,10 @@ public RedisClusterNode(String host, int port, SlotRange slotRange) { super(host, port); + Assert.notNull(slotRange, "SlotRange must not be null"); + this.flags = Collections.emptySet(); - this.slotRange = RedisAssertions.requireNonNull(slotRange,"SlotRange must not be null"); + this.slotRange = slotRange; } /** diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterServerCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterServerCommands.java index 0802c5a7ac..1bcac4c1f7 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/RedisCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisCommands.java index 2b60126aad..1ebb48b83f 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisCommandsProvider.java b/src/main/java/org/springframework/data/redis/connection/RedisCommandsProvider.java index 0a1c7a18ed..24cfc387f9 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisCommandsProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisCommandsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/RedisConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java index 395ccfc85f..053ce917de 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/RedisConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisConnection.java index b3021e64ef..69917d5391 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisConnectionCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionCommands.java index 704da97a77..74875a4bb2 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java index 88f74d8a56..d694630701 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java index 815363f36c..fce11eb2f6 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 8642fc837e..cd99e4a516 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,13 +15,16 @@ */ package org.springframework.data.redis.connection; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** * Hash-specific commands supported by Redis. @@ -29,6 +32,7 @@ * @author Costin Leau * @author Christoph Strobl * @author Mark Paluch + * @author Tihomir Mateev */ public interface RedisHashCommands { @@ -69,7 +73,7 @@ public interface RedisHashCommands { /** * Get values for given {@code fields} from hash at {@code key}. Values are in the order of the requested keys Absent - * field values are represented using {@code null} in the resulting {@link List}. + * field values are represented using {@literal null} in the resulting {@link List}. * * @param key must not be {@literal null}. * @param fields must not be {@literal empty}. @@ -249,4 +253,314 @@ public interface RedisHashCommands { */ @Nullable Long hStrLen(byte[] key, byte[] field); + + /** + * Apply a given {@link org.springframework.data.redis.core.types.Expiration} to the given {@literal fields}. + * + * @param key must not be {@literal null}. + * @param expiration the {@link org.springframework.data.redis.core.types.Expiration} to apply. + * @param fields the names of the {@literal fields} to apply the {@literal expiration} to. + * @return a {@link List} holding the command result for each field in order - {@code 2} indicating the specific field + * is deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration + * time is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no + * such field; + * @since 3.5 + */ + default @Nullable List applyHashFieldExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, byte[]... fields) { + return applyHashFieldExpiration(key, expiration, ExpirationOptions.none(), fields); + } + + /** + * @param key must not be {@literal null}. + * @param expiration the {@link org.springframework.data.redis.core.types.Expiration} to apply. + * @param options additional options to be sent along with the command. + * @param fields the names of the {@literal fields} to apply the {@literal expiration} to. + * @return a {@link List} holding the command result for each field in order - {@code 2} indicating the specific field + * is deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration + * time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT + * condition is not met); {@code -2} indicating there is no such field; + * @since 3.5 + */ + @Nullable + default List applyHashFieldExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, ExpirationOptions options, byte[]... fields) { + + if (expiration.isPersistent()) { + return hPersist(key, fields); + } + + if (ObjectUtils.nullSafeEquals(ExpirationOptions.none(), options)) { + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); + } + return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); + } + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + return hExpire(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + /** + * Set time to live for given {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + default List hExpire(byte[] key, long seconds, byte[]... fields) { + return hExpire(key, seconds, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code fields}. + * + * @param key must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toSeconds() seconds} + * precision, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + default List hExpire(byte[] key, Duration ttl, byte[]... fields) { + return hExpire(key, ttl.toSeconds(), fields); + } + + /** + * Set time to live for given {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields); + + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set ; {@code -2} indicating there is no + * such field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + default List hpExpire(byte[] key, long millis, byte[]... fields) { + return hpExpire(key, millis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toMillis() milliseconds} + * precision, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + default List hpExpire(byte[] key, Duration ttl, byte[]... fields) { + return hpExpire(key, ttl.toMillis(), fields); + } + + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields); + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating + * there is no such field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { + return hExpireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, byte[]... fields); + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating + * there is no such field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + return hpExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields); + + /** + * Remove the expiration from given {@code field}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + List hPersist(byte[] key, byte[]... fields); + + /** + * Get the time to live for {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the field exists but has no associated + * expiration time. The command returns {@code -2} if the field does not exist; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hTtl(byte[] key, byte[]... fields); + + /** + * Get the time to live for {@code fields} in and convert it to the given {@link TimeUnit}. + * + * @param key must not be {@literal null}. + * @param timeUnit must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return for each of the fields supplied - the time to live in the {@link TimeUnit} provided; or a negative value to + * signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. + * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields); + + /** + * Get the time to live for {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hpTtl(byte[] key, byte[]... fields); } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHyperLogLogCommands.java index de24aff2d7..67fa08a4ad 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/RedisInvalidSubscriptionException.java b/src/main/java/org/springframework/data/redis/connection/RedisInvalidSubscriptionException.java index 0af5905aec..d638a90de7 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisInvalidSubscriptionException.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisInvalidSubscriptionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java index ceaf0025be..4319dd8705 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection; import java.time.Duration; +import java.time.Instant; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -25,6 +26,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * Key-specific commands supported by Redis. @@ -180,49 +182,237 @@ default Cursor scan(KeyScanOptions options) { @Nullable Boolean renameNX(byte[] oldKey, byte[] newKey); + /** + * @param key must not be {@literal null}. + * @param expiration the {@link org.springframework.data.redis.core.types.Expiration} to apply. + * @param options additional options to be sent along with the command. + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @since 3.5 + * @see Redis Documentation: EXPIRE + * @see Redis Documentation: PEXPIRE + * @see Redis Documentation: EXPIREAT + * @see Redis Documentation: PEXPIREAT + * @see Redis Documentation: PERSIST + */ + @Nullable + default Boolean applyExpiration(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, + ExpirationOptions options) { + + if (expiration.isPersistent()) { + return persist(key); + } + + if (ObjectUtils.nullSafeEquals(ExpirationOptions.none(), options)) { + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return expireAt(key, expiration.getExpirationTimeInMilliseconds()); + } + return expire(key, expiration.getExpirationTimeInMilliseconds()); + } + if (expiration.isUnixTimestamp()) { + return expireAt(key, expiration.getExpirationTimeInSeconds()); + } + return expire(key, expiration.getExpirationTimeInSeconds()); + } + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return expireAt(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition()); + } + + return expire(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition()); + } + + if (expiration.isUnixTimestamp()) { + return expireAt(key, expiration.getExpirationTimeInSeconds(), options.getCondition()); + } + + return expire(key, expiration.getExpirationTimeInSeconds(), options.getCondition()); + } + /** * Set time to live for given {@code key} in seconds. * * @param key must not be {@literal null}. * @param seconds - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. * @see Redis Documentation: EXPIRE */ @Nullable - Boolean expire(byte[] key, long seconds); + default Boolean expire(byte[] key, long seconds) { + return expire(key, seconds, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set time to live for given {@code key} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: EXPIRE + * @since 3.5 + */ + @Nullable + Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition); + + /** + * Set time to live for given {@code key} using {@link Duration#toSeconds() seconds} precision. + * + * @param key must not be {@literal null}. + * @param duration + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: EXPIRE + * @since 3.5 + */ + @Nullable + default Boolean expire(byte[] key, Duration duration) { + return expire(key, duration.toSeconds()); + } /** * Set time to live for given {@code key} in milliseconds. * * @param key must not be {@literal null}. * @param millis - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: PEXPIRE + */ + @Nullable + default Boolean pExpire(byte[] key, long millis) { + return pExpire(key, millis, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set time to live for given {@code key} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. * @see Redis Documentation: PEXPIRE + * @since 3.5 */ @Nullable - Boolean pExpire(byte[] key, long millis); + Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition); + + /** + * Set time to live for given {@code key} using {@link Duration#toMillis() milliseconds} precision. + * + * @param key must not be {@literal null}. + * @param duration + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: PEXPIRE + * @since 3.5 + */ + @Nullable + default Boolean pExpire(byte[] key, Duration duration) { + return pExpire(key, duration.toMillis()); + } /** * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. * * @param key must not be {@literal null}. * @param unixTime - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: EXPIREAT + */ + @Nullable + default Boolean expireAt(byte[] key, long unixTime) { + return expireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: EXPIREAT + * @since 3.5 + */ + @Nullable + Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition); + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in {@link Instant#getEpochSecond() seconds} + * precision. + * + * @param key must not be {@literal null}. + * @param unixTime + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. * @see Redis Documentation: EXPIREAT + * @since 3.5 */ @Nullable - Boolean expireAt(byte[] key, long unixTime); + default Boolean expireAt(byte[] key, Instant unixTime) { + return expireAt(key, unixTime.getEpochSecond()); + } /** * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds. * * @param key must not be {@literal null}. * @param unixTimeInMillis - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: PEXPIREAT + */ + @Nullable + default Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + return pExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. * @see Redis Documentation: PEXPIREAT + * @since 3.5 */ @Nullable - Boolean pExpireAt(byte[] key, long unixTimeInMillis); + Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition); + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in {@link Instant#toEpochMilli() + * milliseconds} precision. + * + * @param key must not be {@literal null}. + * @param unixTime + * @return {@literal null} when used in pipeline / transaction. {@literal true} if the timeout was set or + * {@literal false} if the timeout was not set; for example, the key doesn't exist, or the operation was + * skipped because of the provided arguments. + * @see Redis Documentation: PEXPIREAT + * @since 3.5 + */ + @Nullable + default Boolean pExpireAt(byte[] key, Instant unixTime) { + return pExpireAt(key, unixTime.toEpochMilli()); + } /** * Remove the expiration from given {@code key}. diff --git a/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java index 8d174c89b8..18852d2f17 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisNode.java b/src/main/java/org/springframework/data/redis/connection/RedisNode.java index ad6841054f..3e117e0d36 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisNode.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -24,10 +24,14 @@ * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch + * @author LeeHyungGeol * @since 1.4 */ public class RedisNode implements NamedNode { + public static final int DEFAULT_PORT = 6379; + public static final int DEFAULT_SENTINEL_PORT = 26379; + @Nullable String id; @Nullable String name; @Nullable String host; @@ -66,8 +70,11 @@ private RedisNode(RedisNode redisNode) { * the port. For example: * *
+	 * RedisNode.fromString("127.0.0.1");
 	 * RedisNode.fromString("127.0.0.1:6379");
+	 * RedisNode.fromString("[aaaa:bbbb::dddd:eeee]");
 	 * RedisNode.fromString("[aaaa:bbbb::dddd:eeee]:6379");
+	 * RedisNode.fromString("my.redis.server");
 	 * RedisNode.fromString("my.redis.server:6379");
 	 * 
* @@ -76,6 +83,27 @@ private RedisNode(RedisNode redisNode) { * @since 2.7.4 */ public static RedisNode fromString(String hostPortString) { + return fromString(hostPortString, DEFAULT_PORT); + } + + /** + * Parse a {@code hostAndPort} string into {@link RedisNode}. Supports IPv4, IPv6, and hostname notations including + * the port. For example: + * + *
+	 * RedisNode.fromString("127.0.0.1");
+	 * RedisNode.fromString("127.0.0.1:6379");
+	 * RedisNode.fromString("[aaaa:bbbb::dddd:eeee]");
+	 * RedisNode.fromString("[aaaa:bbbb::dddd:eeee]:6379");
+	 * RedisNode.fromString("my.redis.server");
+	 * RedisNode.fromString("my.redis.server:6379");
+	 * 
+ * + * @param hostPortString must not be {@literal null} or empty. + * @return the parsed {@link RedisNode}. + * @since 3.4 + */ + public static RedisNode fromString(String hostPortString, int defaultPort) { Assert.notNull(hostPortString, "HostAndPort must not be null"); @@ -94,19 +122,31 @@ public static RedisNode fromString(String hostPortString) { portString = hostPortString.substring(colonPos + 1); } else { // 0 or 2+ colons. Bare hostname or IPv6 literal. - host = hostPortString; + int lastColonIndex = hostPortString.lastIndexOf(':'); + + // IPv6 literal + if (lastColonIndex > hostPortString.indexOf(']')) { + host = hostPortString.substring(0, lastColonIndex); + portString = hostPortString.substring(lastColonIndex + 1); + } else { + // bare hostname + host = hostPortString; + } } } - int port = -1; - try { - port = Integer.parseInt(portString); - } catch (RuntimeException ignore) { - throw new IllegalArgumentException(String.format("Unparseable port number: %s", hostPortString)); + int port = defaultPort; + + if (StringUtils.hasText(portString)) { + try { + port = Integer.parseInt(portString); + } catch (RuntimeException ignore) { + throw new IllegalArgumentException("Unparseable port number: %s".formatted(hostPortString)); + } } if (!isValidPort(port)) { - throw new IllegalArgumentException(String.format("Port number out of range: %s", hostPortString)); + throw new IllegalArgumentException("Port number out of range: %s".formatted(hostPortString)); } return new RedisNode(host, port); @@ -123,14 +163,14 @@ private static String[] getHostAndPortFromBracketedHost(String hostPortString) { if (hostPortString.charAt(0) != '[') { throw new IllegalArgumentException( - String.format("Bracketed host-port string must start with a bracket: %s", hostPortString)); + "Bracketed host-port string must start with a bracket: %s".formatted(hostPortString)); } int colonIndex = hostPortString.indexOf(':'); int closeBracketIndex = hostPortString.lastIndexOf(']'); if (!(colonIndex > -1 && closeBracketIndex > colonIndex)) { - throw new IllegalArgumentException(String.format("Invalid bracketed host/port: %s", hostPortString)); + throw new IllegalArgumentException("Invalid bracketed host/port: %s".formatted(hostPortString)); } String host = hostPortString.substring(1, closeBracketIndex); @@ -138,12 +178,11 @@ private static String[] getHostAndPortFromBracketedHost(String hostPortString) { return new String[] { host, "" }; } else { if (!(hostPortString.charAt(closeBracketIndex + 1) == ':')) { - throw new IllegalArgumentException( - String.format("Only a colon may follow a close bracket: %s", hostPortString)); + throw new IllegalArgumentException("Only a colon may follow a close bracket: %s".formatted(hostPortString)); } for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) { if (!Character.isDigit(hostPortString.charAt(i))) { - throw new IllegalArgumentException(String.format("Port must be numeric: %s", hostPortString)); + throw new IllegalArgumentException("Port must be numeric: %s".formatted(hostPortString)); } } return new String[] { host, hostPortString.substring(closeBracketIndex + 2) }; diff --git a/src/main/java/org/springframework/data/redis/connection/RedisPassword.java b/src/main/java/org/springframework/data/redis/connection/RedisPassword.java index 89c8713ea9..3fcb4ce343 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisPassword.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisPassword.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -140,7 +140,7 @@ public Optional toOptional() { @Override public String toString() { - return String.format("%s[%s]", getClass().getSimpleName(), isPresent() ? "*****" : ""); + return "%s[%s]".formatted(getClass().getSimpleName(), isPresent() ? "*****" : ""); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/RedisPipelineException.java b/src/main/java/org/springframework/data/redis/connection/RedisPipelineException.java index 9a6e31ca16..0f4cd4c9e5 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisPipelineException.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisPipelineException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisPubSubCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisPubSubCommands.java index 6f25eb7daa..00283e80ce 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisPubSubCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisPubSubCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java index 1fd9e96afd..ce54c205cd 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 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/redis/connection/RedisSentinelCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java index 25f3fb666f..0a3882e643 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java index 24b390724d..5839a8f701 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -77,8 +77,9 @@ public RedisSentinelConfiguration() { /** * Creates a new {@link RedisSentinelConfiguration} for given {@link String hostPort} combinations. * - *
-	 * sentinelHostAndPorts[0] = 127.0.0.1:23679 sentinelHostAndPorts[1] = 127.0.0.1:23680 ...
+	 * 
+	 * sentinelHostAndPorts[0] = 127.0.0.1:23679
+	 * sentinelHostAndPorts[1] = 127.0.0.1:23680 ...
 	 * 
* * @param sentinelHostAndPorts must not be {@literal null}. @@ -92,11 +93,9 @@ public RedisSentinelConfiguration(String master, Set sentinelHostAndPort * Creates a new {@link RedisSentinelConfiguration} looking up configuration values from the given * {@link PropertySource}. * - *
-	 * 
+	 * 
 	 * spring.redis.sentinel.master=myMaster
 	 * spring.redis.sentinel.nodes=127.0.0.1:23679,127.0.0.1:23680,127.0.0.1:23681
-	 * 
 	 * 
* * @param propertySource must not be {@literal null}. @@ -150,7 +149,7 @@ public RedisSentinelConfiguration(PropertySource propertySource) { try { database = Integer.parseInt(databaseSource); } catch (NumberFormatException ex) { - throw new IllegalArgumentException(String.format("Invalid DB index '%s'; integer required", databaseSource)); + throw new IllegalArgumentException("Invalid DB index '%s'; integer required".formatted(databaseSource)); } this.setDatabase(database); } @@ -254,7 +253,7 @@ public RedisSentinelConfiguration sentinel(String host, Integer port) { private void appendSentinels(Set hostAndPorts) { for (String hostAndPort : hostAndPorts) { - addSentinel(RedisNode.fromString(hostAndPort)); + addSentinel(RedisNode.fromString(hostAndPort, RedisNode.DEFAULT_SENTINEL_PORT)); } } @@ -266,7 +265,7 @@ public int getDatabase() { @Override public void setDatabase(int index) { - Assert.isTrue(index >= 0, () -> String.format("Invalid DB index '%d'; non-negative index required", index)); + Assert.isTrue(index >= 0, "Invalid DB index '%d'; non-negative index required".formatted(index)); this.database = index; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java index 0d84cc2107..34f0db6003 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/RedisServer.java b/src/main/java/org/springframework/data/redis/connection/RedisServer.java index 1a8b306c45..bd363598c6 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisServer.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/RedisServerCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java index 26a610e05c..f21e6281fd 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisSetCommands.java index bd3e75deb8..26dcb1976f 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisSocketConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java index 7e70cfdfd4..e48799a659 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -74,7 +74,7 @@ public int getDatabase() { @Override public void setDatabase(int index) { - Assert.isTrue(index >= 0, () -> String.format("Invalid DB index '%s' (a positive index required)", index)); + Assert.isTrue(index >= 0, () -> "Invalid DB index '%s'; non-negative index required".formatted(index)); this.database = index; } @@ -108,10 +108,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof RedisSocketConfiguration)) { + if (!(o instanceof RedisSocketConfiguration that)) { return false; } - RedisSocketConfiguration that = (RedisSocketConfiguration) o; if (database != that.database) { return false; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java index 0ec52a6b70..8acb2a3be8 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -70,7 +70,7 @@ public RedisStandaloneConfiguration(String hostName, int port) { Assert.hasText(hostName, "Host name must not be null or empty"); Assert.isTrue(port >= 1 && port <= 65535, - () -> String.format("Port %d must be a valid TCP port in the range between 1-65535", port)); + "Port %d must be a valid TCP port in the range between 1-65535".formatted(port)); this.hostName = hostName; this.port = port; @@ -103,7 +103,7 @@ public int getDatabase() { @Override public void setDatabase(int index) { - Assert.isTrue(index >= 0, () -> String.format("Invalid DB index '%s' (a positive index required)", index)); + Assert.isTrue(index >= 0, "Invalid DB index '%d'; non-negative index required".formatted(index)); this.database = index; } @@ -137,10 +137,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof RedisStandaloneConfiguration)) { + if (!(o instanceof RedisStandaloneConfiguration that)) { return false; } - RedisStandaloneConfiguration that = (RedisStandaloneConfiguration) o; if (port != that.port) { return false; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java index 6ed05903b1..66dd6f6051 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -118,7 +118,7 @@ public int getDatabase() { @Override public void setDatabase(int index) { - Assert.isTrue(index >= 0, () -> String.format("Invalid DB index '%s' (a positive index required)", index)); + Assert.isTrue(index >= 0, "Invalid DB index '%d'; non-negative index required".formatted(index)); this.database = index; this.nodes.forEach(it -> it.setDatabase(database)); @@ -159,10 +159,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof RedisStaticMasterReplicaConfiguration)) { + if (!(o instanceof RedisStaticMasterReplicaConfiguration that)) { return false; } - RedisStaticMasterReplicaConfiguration that = (RedisStaticMasterReplicaConfiguration) o; if (database != that.database) { return false; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStreamCommands.java index a326106610..8385d70d34 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -214,7 +214,7 @@ public Long getMaxlen() { * @return {@literal true} if {@literal MAXLEN} is set. */ public boolean hasMaxlen() { - return maxlen != null && maxlen > 0; + return maxlen != null; } /** @@ -246,10 +246,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof XAddOptions)) { + if (!(o instanceof XAddOptions that)) { return false; } - XAddOptions that = (XAddOptions) o; if (nomkstream != that.nomkstream) { return false; } @@ -789,19 +788,28 @@ public static XPendingOptions unbounded() { /** * Create new {@link XPendingOptions} with an unbounded {@link Range} ({@literal - +}). * - * @param count the max number of messages to return. Must not be {@literal null}. + * @param count the max number of messages to return. Must not be negative. * @return new instance of {@link XPendingOptions}. */ public static XPendingOptions unbounded(Long count) { + + Assert.isTrue(count > -1, "Count must not be negative"); + return new XPendingOptions(null, Range.unbounded(), count); } /** * Create new {@link XPendingOptions} with given {@link Range} and limit. * + * @param range must not be {@literal null}. + * @param count the max number of messages to return. Must not be negative. * @return new instance of {@link XPendingOptions}. */ public static XPendingOptions range(Range range, Long count) { + + Assert.notNull(range, "Range must not be null"); + Assert.isTrue(count > -1, "Count must not be negative"); + return new XPendingOptions(null, range, count); } @@ -849,7 +857,7 @@ public boolean hasConsumer() { * @return {@literal true} count is set. */ public boolean isLimited() { - return count != null && count > -1; + return count != null; } } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index beac673984..f591be4245 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -29,6 +29,7 @@ * @author Costin Leau * @author Christoph Strobl * @author Mark Paluch + * @author Marcin Grzejszczak */ public interface RedisStringCommands { @@ -86,10 +87,10 @@ enum BitOperation { /** * Get multiple {@code keys}. Values are in the order of the requested keys Absent field values are represented using - * {@code null} in the resulting {@link List}. + * {@literal null} in the resulting {@link List}. * * @param keys must not be {@literal null}. - * @return {@code null} when used in pipeline / transaction. + * @return {@literal null} when used in pipeline / transaction. * @see Redis Documentation: MGET */ @Nullable @@ -122,6 +123,22 @@ enum BitOperation { @Nullable Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option); + /** + * Set {@code value} for {@code key}. Return the old string stored at key, or {@literal null} if key did not exist. An + * error is returned and SET aborted if the value stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl or + * {@link Expiration#keepTtl()} to keep the existing expiration. + * @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non-existing. + * @return {@literal null} when used in pipeline / transaction. + * @since 3.5 + * @see Redis Documentation: SET + */ + @Nullable + byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option); + /** * Set {@code value} for {@code key}, only if {@code key} does not exist. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSubscribedConnectionException.java b/src/main/java/org/springframework/data/redis/connection/RedisSubscribedConnectionException.java index 56ce5a9f35..846a935c4d 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSubscribedConnectionException.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSubscribedConnectionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisTxCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisTxCommands.java index 8e3578dbc2..56ad3634ea 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisTxCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisTxCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/RedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java index 8578d688f6..6ad0ecd59b 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/ReturnType.java b/src/main/java/org/springframework/data/redis/connection/ReturnType.java index 5ed9e24dbf..b39a9684ce 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReturnType.java +++ b/src/main/java/org/springframework/data/redis/connection/ReturnType.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -46,12 +46,12 @@ public enum ReturnType { MULTI, /** - * Returned as {@literal byte[]} + * Returned as {@code byte[]} */ STATUS, /** - * Returned as {@literal byte[]} + * Returned as {@code byte[]} */ VALUE; @@ -73,7 +73,11 @@ public static ReturnType fromJavaType(@Nullable Class javaType) { return ReturnType.BOOLEAN; } - if (ClassUtils.isAssignable(Long.class, javaType)) { + if (ClassUtils.isAssignable(Double.class, javaType) || ClassUtils.isAssignable(Float.class, javaType)) { + return ReturnType.VALUE; + } + + if (ClassUtils.isAssignable(Number.class, javaType)) { return ReturnType.INTEGER; } diff --git a/src/main/java/org/springframework/data/redis/connection/SentinelMasterId.java b/src/main/java/org/springframework/data/redis/connection/SentinelMasterId.java index e5d72b171c..ce778884d7 100644 --- a/src/main/java/org/springframework/data/redis/connection/SentinelMasterId.java +++ b/src/main/java/org/springframework/data/redis/connection/SentinelMasterId.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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. @@ -51,10 +51,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof SentinelMasterId)) { + if (!(o instanceof SentinelMasterId that)) { return false; } - SentinelMasterId that = (SentinelMasterId) o; return ObjectUtils.nullSafeEquals(name, that.name); } diff --git a/src/main/java/org/springframework/data/redis/connection/SortParameters.java b/src/main/java/org/springframework/data/redis/connection/SortParameters.java index b90d56c6a9..27804b98fe 100644 --- a/src/main/java/org/springframework/data/redis/connection/SortParameters.java +++ b/src/main/java/org/springframework/data/redis/connection/SortParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index e198eecfd3..dfcc585130 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -71,7 +71,6 @@ * @author Andrey Shlykov * @author ihaohong * @author Shyngys Sapraliyev - * * @see RedisCallback * @see RedisSerializer * @see StringRedisTemplate @@ -225,29 +224,74 @@ interface StringTuple extends Tuple { * @see Redis Documentation: EXPIRE * @see RedisKeyCommands#expire(byte[], long) */ - Boolean expire(String key, long seconds); + default Boolean expire(String key, long seconds) { + return expire(key, seconds, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set time to live for given {@code key} in seconds. + * + * @param key must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param seconds + * @return + * @since 3.5 + * @see Redis Documentation: EXPIRE + * @see RedisKeyCommands#expire(byte[], long) + */ + Boolean expire(String key, long seconds, ExpirationOptions.Condition condition); + + /** + * Set time to live for given {@code key} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis + * @return + * @see Redis Documentation: PEXPIRE + * @see RedisKeyCommands#pExpire(byte[], long) + */ + default Boolean pExpire(String key, long millis) { + return pExpire(key, millis, ExpirationOptions.Condition.ALWAYS); + } /** * Set time to live for given {@code key} in milliseconds. * * @param key must not be {@literal null}. * @param millis + * @param condition the condition for expiration, must not be {@literal null}. * @return + * @since 3.5 * @see Redis Documentation: PEXPIRE * @see RedisKeyCommands#pExpire(byte[], long) */ - Boolean pExpire(String key, long millis); + Boolean pExpire(String key, long millis, ExpirationOptions.Condition condition); + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime + * @return + * @see Redis Documentation: EXPIREAT + * @see RedisKeyCommands#expireAt(byte[], long) + */ + default Boolean expireAt(String key, long unixTime) { + return expireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS); + } /** * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. * * @param key must not be {@literal null}. * @param unixTime + * @param condition the condition for expiration, must not be {@literal null}. * @return + * @since 3.5 * @see Redis Documentation: EXPIREAT * @see RedisKeyCommands#expireAt(byte[], long) */ - Boolean expireAt(String key, long unixTime); + Boolean expireAt(String key, long unixTime, ExpirationOptions.Condition condition); /** * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds. @@ -258,7 +302,22 @@ interface StringTuple extends Tuple { * @see Redis Documentation: PEXPIREAT * @see RedisKeyCommands#pExpireAt(byte[], long) */ - Boolean pExpireAt(String key, long unixTimeInMillis); + default Boolean pExpireAt(String key, long unixTimeInMillis) { + return pExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS); + } + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis + * @param condition the condition for expiration, must not be {@literal null}. + * @return + * @since 3.5 + * @see Redis Documentation: PEXPIREAT + * @see RedisKeyCommands#pExpireAt(byte[], long) + */ + Boolean pExpireAt(String key, long unixTimeInMillis, ExpirationOptions.Condition condition); /** * Remove the expiration from given {@code key}. @@ -1661,7 +1720,6 @@ default Long lPos(String key, String element) { */ Long zRemRange(String key, long start, long end); - /** * Remove all elements between the lexicographical {@link Range}. * @@ -1941,7 +1999,8 @@ default Set zUnionWithScores(Aggregate aggregate, int[] weights, St * @return * @since 1.6 * @see Redis Documentation: ZRANGEBYLEX - * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -1983,7 +2042,8 @@ default Set zRevRangeByLex(String key, org.springframework.data.domain.R * @return * @since 2.4 * @see Redis Documentation: ZREVRANGEBYLEX - * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRevRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -2333,6 +2393,208 @@ Long zRangeStoreRevByScore(String dstKey, String srcKey, @Nullable Long hStrLen(String key, String field); + /** + * Set time to live for given {@code field} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + default List hExpire(String key, long seconds, String... fields) { + return hExpire(key, seconds, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + List hExpire(String key, long seconds, ExpirationOptions.Condition condition, String... fields); + + /** + * Set time to live for given {@code field} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + default List hpExpire(String key, long millis, String... fields) { + return hpExpire(key, millis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(String key, long millis, ExpirationOptions.Condition condition, String... fields); + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating + * there is no such field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + default List hExpireAt(String key, long unixTime, String... fields) { + return hExpireAt(key, unixTime, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(String key, long unixTime, ExpirationOptions.Condition condition, String... fields); + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set; {@code -2} indicating + * there is no such field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + default List hpExpireAt(String key, long unixTimeInMillis, String... fields) { + return hpExpireAt(key, unixTimeInMillis, ExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(String key, long unixTimeInMillis, ExpirationOptions.Condition condition, + String... fields); + + /** + * Remove the expiration from given {@code field}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + List hPersist(String key, String... fields); + + /** + * Get the time to live for {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hTtl(String key, String... fields); + + /** + * Get the time to live for {@code fields} in and convert it to the given {@link TimeUnit}. + * + * @param key must not be {@literal null}. + * @param timeUnit must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in the {@link TimeUnit} + * provided; or a negative value to signal an error. The command returns {@code -1} if the key exists but has + * no associated expiration time. The command returns {@code -2} if the key does not exist; {@literal null} + * when used in pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hTtl(String key, TimeUnit timeUnit, String... fields); + + /** + * Get the time to live for {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + List hpTtl(String key, String... fields); + // ------------------------------------------------------------------------- // Methods dealing with HyperLogLog // ------------------------------------------------------------------------- @@ -2556,8 +2818,7 @@ GeoResults> geoRadiusByMember(String key, String member, Dis /** * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape - * shape}. The query's center point is provided by - * {@link GeoReference}. + * shape}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. @@ -2573,8 +2834,7 @@ GeoResults> geoSearch(String key, GeoReference refer /** * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} - * and store the result at {@code destKey}. The query's center point is provided by - * {@link GeoReference}. + * and store the result at {@code destKey}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/redis/connection/Subscription.java b/src/main/java/org/springframework/data/redis/connection/Subscription.java index 8fbbb32239..96510df79c 100644 --- a/src/main/java/org/springframework/data/redis/connection/Subscription.java +++ b/src/main/java/org/springframework/data/redis/connection/Subscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/SubscriptionListener.java b/src/main/java/org/springframework/data/redis/connection/SubscriptionListener.java index 5049ef2423..8d9e9d6b00 100644 --- a/src/main/java/org/springframework/data/redis/connection/SubscriptionListener.java +++ b/src/main/java/org/springframework/data/redis/connection/SubscriptionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/ValueEncoding.java b/src/main/java/org/springframework/data/redis/connection/ValueEncoding.java index 1185756bf3..b0d573639a 100644 --- a/src/main/java/org/springframework/data/redis/connection/ValueEncoding.java +++ b/src/main/java/org/springframework/data/redis/connection/ValueEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java index 98ec282faf..2d4eb2cb59 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -20,11 +20,10 @@ import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; @@ -63,6 +62,7 @@ * @author daihuabin * @author John Blum * @author Sorokin Evgeniy + * @author Marcin Grzejszczak */ public abstract class Converters { @@ -410,7 +410,7 @@ public static Object parse(Object source, String sourcePath, Map { *
  • {@code %s:%i} (Redis 3)
  • *
  • {@code %s:%i@%i} (Redis 4, with bus port)
  • *
  • {@code %s:%i@%i,%s} (Redis 7, with announced hostname)
  • + * + * The output of the {@code CLUSTER NODES } command is just a space-separated CSV string, where each + * line represents a node in the cluster. The following is an example of output on Redis 7.2.0. + * You can check the latest here. + * + * {@code ... } + * * */ - static final Pattern clusterEndpointPattern = Pattern - .compile("\\[?([0-9a-zA-Z\\-_\\.:]*)\\]?:([0-9]+)(?:@[0-9]+(?:,([^,].*))?)?"); private static final Map flagLookupMap; static { @@ -567,32 +572,88 @@ enum ClusterNodesConverter implements Converter { static final int LINK_STATE_INDEX = 7; static final int SLOTS_INDEX = 8; + /** + * Value object capturing Redis' representation of a cluster node network coordinate. + * + * @author Marcin Grzejszczak + * @author Mark Paluch + */ + record AddressPortHostname(String address, String port, @Nullable String hostname) { + + /** + * Parses Redis {@code CLUSTER NODES} host and port segment into {@link AddressPortHostname}. + */ + static AddressPortHostname parse(String hostAndPortPart) { + + String[] segments = hostAndPortPart.split(","); + int portSeparator = segments[0].lastIndexOf(":"); + Assert.isTrue(portSeparator != -1, "ClusterNode information does not define host and port"); + + String addressPart = getAddressPart(segments[0].substring(0, portSeparator)); + String portPart = getPortPart(segments[0].substring(portSeparator + 1)); + String hostnamePart = segments.length > 1 ? segments[1] : null; + + return new AddressPortHostname(addressPart, portPart, hostnamePart); + } + + private static String getAddressPart(String address) { + return address.startsWith("[") && address.endsWith("]") ? address.substring(1, address.length() - 1) : address; + } + + private static String getPortPart(String segment) { + + if (segment.contains("@")) { + return segment.substring(0, segment.indexOf('@')); + } + + if (segment.contains(":")) { + return segment.substring(0, segment.indexOf(':')); + } + + return segment; + } + + public int portAsInt() { + return Integer.parseInt(port()); + } + + public boolean hasHostname() { + return StringUtils.hasText(hostname()); + } + + public String getRequiredHostname() { + + if (StringUtils.hasText(hostname())) { + return hostname(); + } + + throw new IllegalStateException("Hostname not available"); + } + } + @Override public RedisClusterNode convert(String source) { String[] args = source.split(" "); - Matcher matcher = clusterEndpointPattern.matcher(args[HOST_PORT_INDEX]); + Assert.isTrue(args.length >= MASTER_ID_INDEX + 1, + () -> "Invalid ClusterNode information, insufficient segments: %s".formatted(source)); - Assert.isTrue(matcher.matches(), "ClusterNode information does not define host and port"); - - String addressPart = matcher.group(1); - String portPart = matcher.group(2); - String hostnamePart = matcher.group(3); + AddressPortHostname endpoint = AddressPortHostname.parse(args[HOST_PORT_INDEX]); SlotRange range = parseSlotRange(args); - Set flags = parseFlags(args); + Set flags = parseFlags(args[FLAGS_INDEX]); RedisClusterNodeBuilder nodeBuilder = RedisClusterNode.newRedisClusterNode() - .listeningAt(addressPart, Integer.parseInt(portPart)) // + .listeningAt(endpoint.address(), endpoint.portAsInt()) // .withId(args[ID_INDEX]) // .promotedAs(flags.contains(Flag.MASTER) ? NodeType.MASTER : NodeType.REPLICA) // .serving(range) // .withFlags(flags) // .linkState(parseLinkState(args)); - if (hostnamePart != null) { - nodeBuilder.withName(hostnamePart); + if (endpoint.hasHostname()) { + nodeBuilder.withName(endpoint.getRequiredHostname()); } if (!args[MASTER_ID_INDEX].isEmpty() && !args[MASTER_ID_INDEX].startsWith("-")) { @@ -602,14 +663,12 @@ public RedisClusterNode convert(String source) { return nodeBuilder.build(); } - private Set parseFlags(String[] args) { - - String raw = args[FLAGS_INDEX]; + private Set parseFlags(String source) { Set flags = new LinkedHashSet<>(8, 1); - if (StringUtils.hasText(raw)) { - for (String flag : raw.split(",")) { + if (StringUtils.hasText(source)) { + for (String flag : source.split(",")) { flags.add(flagLookupMap.get(flag)); } } diff --git a/src/main/java/org/springframework/data/redis/connection/convert/ListConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/ListConverter.java index d9b87a8c86..1a384f1af7 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/ListConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/ListConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/LongToBooleanConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/LongToBooleanConverter.java index 0e708780bc..8686258820 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/LongToBooleanConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/LongToBooleanConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/MapConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/MapConverter.java index 2d9bafca65..1f09154cba 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/MapConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/MapConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/MapToPropertiesConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java index 2e1e93e155..b25e587e0a 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/convert/SetConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/SetConverter.java index 244306125e..c51002e368 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/SetConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/SetConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/StringToDataTypeConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/StringToDataTypeConverter.java index 2d1ea32cc3..39214821ca 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/StringToDataTypeConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/StringToDataTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/StringToPropertiesConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/StringToPropertiesConverter.java index 7f8889eabc..16725f049c 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/StringToPropertiesConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/StringToPropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/convert/StringToRedisClientInfoConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/StringToRedisClientInfoConverter.java index d2bf3aa39e..4c01ddfe6a 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/StringToRedisClientInfoConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/StringToRedisClientInfoConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/convert/TransactionResultConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/TransactionResultConverter.java index 883ceb9e15..2655245ceb 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/TransactionResultConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/TransactionResultConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/jedis/DefaultJedisClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java index 50fd68211e..fee76a291f 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +34,7 @@ */ class DefaultJedisClientConfiguration implements JedisClientConfiguration { + private final Optional customizer; private final boolean useSsl; private final Optional sslSocketFactory; private final Optional sslParameters; @@ -44,11 +45,13 @@ class DefaultJedisClientConfiguration implements JedisClientConfiguration { private final Duration readTimeout; private final Duration connectTimeout; - DefaultJedisClientConfiguration(boolean useSsl, @Nullable SSLSocketFactory sslSocketFactory, + DefaultJedisClientConfiguration(@Nullable JedisClientConfigBuilderCustomizer customizer, boolean useSsl, + @Nullable SSLSocketFactory sslSocketFactory, @Nullable SSLParameters sslParameters, @Nullable HostnameVerifier hostnameVerifier, boolean usePooling, @Nullable GenericObjectPoolConfig poolConfig, @Nullable String clientName, Duration readTimeout, Duration connectTimeout) { + this.customizer = Optional.ofNullable(customizer); this.useSsl = useSsl; this.sslSocketFactory = Optional.ofNullable(sslSocketFactory); this.sslParameters = Optional.ofNullable(sslParameters); @@ -60,6 +63,11 @@ class DefaultJedisClientConfiguration implements JedisClientConfiguration { this.connectTimeout = connectTimeout; } + @Override + public Optional getCustomizer() { + return customizer; + } + @Override public boolean isUseSsl() { return useSsl; diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfigBuilderCustomizer.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfigBuilderCustomizer.java new file mode 100644 index 0000000000..44e8855bf8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfigBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024-2025 the original author 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.redis.connection.jedis; + +import redis.clients.jedis.DefaultJedisClientConfig; + +/** + * Strategy interface for customizing {@link DefaultJedisClientConfig.Builder JedisClientConfig}. Any ClientConfig will + * be used to call this interface implementation so you can set the protocol, client name, etc. after Spring has applies + * its defaults. + * + * @author Mark Paluch + * @since 3.4 + * @see redis.clients.jedis.DefaultJedisClientConfig.Builder + */ +@FunctionalInterface +public interface JedisClientConfigBuilderCustomizer { + + /** + * Customize the {@link DefaultJedisClientConfig.Builder}. + * + * @param builder the builder to customize. + */ + void customize(DefaultJedisClientConfig.Builder builder); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java index 1c9628db0c..6cf9357dfa 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -58,6 +58,12 @@ */ public interface JedisClientConfiguration { + /** + * @return the optional {@link JedisClientConfigBuilderCustomizer}. + * @since 3.4 + */ + Optional getCustomizer(); + /** * @return {@literal true} to use SSL, {@literal false} to use unencrypted connections. */ @@ -119,6 +125,8 @@ static JedisClientConfigurationBuilder builder() { /** * Creates a default {@link JedisClientConfiguration}. *
    + *
    Customizer
    + *
    none
    *
    SSL enabled
    *
    no
    *
    Pooling enabled
    @@ -142,6 +150,15 @@ static JedisClientConfiguration defaultConfiguration() { */ interface JedisClientConfigurationBuilder { + /** + * Configure a {@link JedisClientConfigBuilderCustomizer} to configure + * {@link redis.clients.jedis.JedisClientConfig}. + * + * @return {@link JedisClientConfigurationBuilder}. + * @since 3.4 + */ + JedisClientConfigurationBuilder customize(JedisClientConfigBuilderCustomizer customizer); + /** * Enable SSL connections. * @@ -269,6 +286,7 @@ interface JedisSslClientConfigurationBuilder { class DefaultJedisClientConfigurationBuilder implements JedisClientConfigurationBuilder, JedisPoolingClientConfigurationBuilder, JedisSslClientConfigurationBuilder { + private @Nullable JedisClientConfigBuilderCustomizer customizer; private boolean useSsl; private @Nullable SSLSocketFactory sslSocketFactory; private @Nullable SSLParameters sslParameters; @@ -281,6 +299,15 @@ class DefaultJedisClientConfigurationBuilder implements JedisClientConfiguration private DefaultJedisClientConfigurationBuilder() {} + @Override + public JedisClientConfigurationBuilder customize(JedisClientConfigBuilderCustomizer customizer) { + + Assert.notNull(customizer, "JedisClientConfigBuilderCustomizer must not be null"); + + this.customizer = customizer; + return this; + } + @Override public JedisSslClientConfigurationBuilder useSsl() { @@ -366,8 +393,8 @@ public JedisClientConfigurationBuilder connectTimeout(Duration connectTimeout) { @Override public JedisClientConfiguration build() { - return new DefaultJedisClientConfiguration(useSsl, sslSocketFactory, sslParameters, hostnameVerifier, usePooling, - poolConfig, clientName, readTimeout, connectTimeout); + return new DefaultJedisClientConfiguration(customizer, useSsl, sslSocketFactory, sslParameters, hostnameVerifier, + usePooling, poolConfig, clientName, readTimeout, connectTimeout); } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java index af7707d9ae..ab8d1d072b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index 5b68de45e9..51f6f3cd14 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,8 @@ import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisClusterInfoCache; +import redis.clients.jedis.Protocol; import redis.clients.jedis.providers.ClusterConnectionProvider; import java.time.Duration; @@ -761,14 +763,19 @@ public Jedis getResourceForSpecificNode(RedisClusterNode node) { return new Jedis(connection); } - throw new DataAccessResourceFailureException(String.format("Node %s is unknown to cluster", node)); + throw new DataAccessResourceFailureException("Node %s is unknown to cluster".formatted(node)); } + @Nullable private ConnectionPool getResourcePoolForSpecificNode(RedisClusterNode node) { Map clusterNodes = cluster.getClusterNodes(); - if (clusterNodes.containsKey(node.asString())) { - return clusterNodes.get(node.asString()); + HostAndPort hap = new HostAndPort(node.getHost(), + node.getPort() == null ? Protocol.DEFAULT_PORT : node.getPort()); + String key = JedisClusterInfoCache.getNodeKey(hap); + + if (clusterNodes.containsKey(key)) { + return clusterNodes.get(key); } return null; @@ -779,8 +786,8 @@ private Connection getConnectionForSpecificNode(RedisClusterNode node) { RedisClusterNode member = topologyProvider.getTopology().lookup(node); if (!member.hasValidHost()) { - throw new DataAccessResourceFailureException(String - .format("Cannot obtain connection to node %ss as it is not associated with a hostname", node.getId())); + throw new DataAccessResourceFailureException( + "Cannot obtain connection to node %ss; " + "it is not associated with a hostname".formatted(node.getId())); } if (member != null && connectionHandler != null) { @@ -805,13 +812,11 @@ public void returnResourceForSpecificNode(RedisClusterNode node, Object client) */ public static class JedisClusterTopologyProvider implements ClusterTopologyProvider { - private long time = 0; + private final JedisCluster cluster; private final long cacheTimeMs; - private @Nullable ClusterTopology cached; - - private final JedisCluster cluster; + private volatile @Nullable JedisClusterTopology cached; /** * Create new {@link JedisClusterTopologyProvider}. Uses a default cache timeout of 100 milliseconds. @@ -842,12 +847,12 @@ public JedisClusterTopologyProvider(JedisCluster cluster, Duration cacheTimeout) @Override public ClusterTopology getTopology() { - if (cached != null && shouldUseCachedValue()) { - return cached; + JedisClusterTopology topology = cached; + if (shouldUseCachedValue(topology)) { + return topology; } Map errors = new LinkedHashMap<>(); - List> list = new ArrayList<>(cluster.getClusterNodes().entrySet()); Collections.shuffle(list); @@ -856,13 +861,9 @@ public ClusterTopology getTopology() { try (Connection connection = entry.getValue().getResource()) { - time = System.currentTimeMillis(); - Set nodes = Converters.toSetOfRedisClusterNodes(new Jedis(connection).clusterNodes()); - - cached = new ClusterTopology(nodes); - - return cached; + topology = cached = new JedisClusterTopology(nodes, System.currentTimeMillis(), cacheTimeMs); + return topology; } catch (Exception ex) { errors.put(entry.getKey(), ex); @@ -872,7 +873,7 @@ public ClusterTopology getTopology() { StringBuilder stringBuilder = new StringBuilder(); for (Entry entry : errors.entrySet()) { - stringBuilder.append(String.format("\r\n\t- %s failed: %s", entry.getKey(), entry.getValue().getMessage())); + stringBuilder.append("\r\n\t- %s failed: %s".formatted(entry.getKey(), entry.getValue().getMessage())); } throw new ClusterStateFailureException( @@ -887,9 +888,54 @@ public ClusterTopology getTopology() { * topology. * @see #JedisClusterTopologyProvider(JedisCluster, Duration) * @since 2.2 + * @deprecated since 3.3.4, use {@link #shouldUseCachedValue(JedisClusterTopology)} instead. */ + @Deprecated(since = "3.3.4", forRemoval = true) protected boolean shouldUseCachedValue() { - return time + cacheTimeMs > System.currentTimeMillis(); + return shouldUseCachedValue(cached); + } + + /** + * Returns whether {@link #getTopology()} should return the cached {@link JedisClusterTopology}. Uses a time-based + * caching. + * + * @return {@literal true} to use the cached {@link ClusterTopology}; {@literal false} to fetch a new cluster + * topology. + * @see #JedisClusterTopologyProvider(JedisCluster, Duration) + * @since 3.3.4 + */ + protected boolean shouldUseCachedValue(@Nullable JedisClusterTopology topology) { + return topology != null && topology.getMaxTime() > System.currentTimeMillis(); + } + } + + protected static class JedisClusterTopology extends ClusterTopology { + + private final long time; + private final long timeoutMs; + + JedisClusterTopology(Set nodes, long creationTimeMs, long timeoutMs) { + super(nodes); + this.time = creationTimeMs; + this.timeoutMs = timeoutMs; + } + + /** + * Get the time in ms when the {@link ClusterTopology} was captured. + * + * @return ClusterTopology time. + */ + public long getTime() { + return time; + } + + /** + * Get the maximum time in ms the {@link ClusterTopology} should be used before a refresh is required. + * + * @return ClusterTopology maximum age. + */ + long getMaxTime() { + return time + timeoutMs; } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java index 1d98726325..35c7db73f3 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 1803da058a..551a17d153 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; @@ -23,8 +24,10 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanCursor; @@ -39,6 +42,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Tihomir Mateev * @since 2.0 */ class JedisClusterHashCommands implements RedisHashCommands { @@ -279,14 +283,136 @@ protected ScanIteration> doScan(CursorId cursorId, ScanOpt ScanParams params = JedisConverters.toScanParams(options); - ScanResult> result = connection.getCluster().hscan(key, - JedisConverters.toBytes(cursorId), + ScanResult> result = connection.getCluster().hscan(key, JedisConverters.toBytes(cursorId), params); return new ScanIteration<>(CursorId.of(result.getCursor()), result.getResult()); } }.open(); } + @Override + public List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpire(key, seconds, fields); + } + + return connection.getCluster().hexpire(key, seconds, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpire(key, millis, fields); + } + + return connection.getCluster().hpexpire(key, millis, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpireAt(key, unixTime, fields); + } + + return connection.getCluster().hexpireAt(key, unixTime, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, fields); + } + + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hPersist(byte[] key, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + return connection.getCluster().hpersist(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hTtl(byte[] key, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + return connection.getCluster().httl(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + return connection.getCluster().httl(key, fields).stream() + .map(it -> it != null ? timeUnit.convert(it, TimeUnit.SECONDS) : null).toList(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpTtl(byte[] key, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + return connection.getCluster().hpttl(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + @Nullable @Override public Long hStrLen(byte[] key, byte[] field) { @@ -298,7 +424,7 @@ public Long hStrLen(byte[] key, byte[] field) { } private DataAccessException convertJedisAccessException(Exception ex) { - return connection.convertJedisAccessException(ex); } + } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHyperLogLogCommands.java index 87fdfc25d9..dd77b33f58 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java index f431d362ec..559c15bf86 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +16,7 @@ package org.springframework.data.redis.connection.jedis; import redis.clients.jedis.Jedis; +import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.RestoreParams; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; @@ -35,6 +36,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.ClusterSlotHashUtil; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisKeyCommands; import org.springframework.data.redis.connection.RedisNode; @@ -274,48 +276,67 @@ public Boolean renameNX(byte[] sourceKey, byte[] targetKey) { } @Override - public Boolean expire(byte[] key, long seconds) { + public Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); try { - return JedisConverters.toBoolean(connection.getCluster().expire(key, seconds)); + if (condition == ExpirationOptions.Condition.ALWAYS) { + return JedisConverters.toBoolean(connection.getCluster().expire(key, seconds)); + } + + return JedisConverters + .toBoolean(connection.getCluster().expire(key, seconds, ExpiryOption.valueOf(condition.name()))); } catch (Exception ex) { throw convertJedisAccessException(ex); } } @Override - public Boolean pExpire(byte[] key, long millis) { + public Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); try { - return JedisConverters.toBoolean(connection.getCluster().pexpire(key, millis)); + if (condition == ExpirationOptions.Condition.ALWAYS) { + return JedisConverters.toBoolean(connection.getCluster().pexpire(key, millis)); + } + return JedisConverters + .toBoolean(connection.getCluster().pexpire(key, millis, ExpiryOption.valueOf(condition.name()))); } catch (Exception ex) { throw convertJedisAccessException(ex); } } @Override - public Boolean expireAt(byte[] key, long unixTime) { + public Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); try { - return JedisConverters.toBoolean(connection.getCluster().expireAt(key, unixTime)); + if (condition == ExpirationOptions.Condition.ALWAYS) { + return JedisConverters.toBoolean(connection.getCluster().expireAt(key, unixTime)); + } + + return JedisConverters + .toBoolean(connection.getCluster().expireAt(key, unixTime, ExpiryOption.valueOf(condition.name()))); } catch (Exception ex) { throw convertJedisAccessException(ex); } } @Override - public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + public Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); try { - return JedisConverters.toBoolean(connection.getCluster().pexpireAt(key, unixTimeInMillis)); + if (condition == ExpirationOptions.Condition.ALWAYS) { + return JedisConverters.toBoolean(connection.getCluster().pexpireAt(key, unixTimeInMillis)); + } + + return JedisConverters + .toBoolean(connection.getCluster().pexpireAt(key, unixTimeInMillis, ExpiryOption.valueOf(condition.name()))); } catch (Exception ex) { throw convertJedisAccessException(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java index 81f93645b8..3b259f36e1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java index 727c4536d9..9e845f032e 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterServerCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterServerCommands.java index c0b61ae75d..1c6045faf9 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -347,10 +347,11 @@ public Long time(RedisClusterNode node, TimeUnit timeUnit) { public void killClient(String host, int port) { Assert.hasText(host, "Host for 'CLIENT KILL' must not be 'null' or 'empty'"); - String hostAndPort = String.format("%s:%s", host, port); + String hostAndPort = "%s:%d".formatted(host, port); - connection.getClusterCommandExecutor() - .executeCommandOnAllNodes((JedisClusterCommandCallback) client -> client.clientKill(hostAndPort)); + JedisClusterCommandCallback command = client -> client.clientKill(hostAndPort); + + connection.getClusterCommandExecutor().executeCommandOnAllNodes(command); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterSetCommands.java index 4be0422e03..c25def525f 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStreamCommands.java index 05cadae499..9d26a6cd8d 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/jedis/JedisClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java index 514ce81579..af51cafe3d 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -42,6 +42,7 @@ * @author Mark Paluch * @author Xiaohu Zhang * @author dengliming + * @author Marcin Grzejszczak * @since 2.0 */ class JedisClusterStringCommands implements RedisStringCommands { @@ -150,6 +151,24 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op } } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + SetParams setParams = JedisConverters.toSetCommandExPxArgument(expiration, + JedisConverters.toSetCommandNxXxArgument(option)); + + try { + return connection.getCluster().setGet(key, value, setParams); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java index 156fb87fb5..b7bc9b5f92 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -908,8 +908,9 @@ public Set zInterWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + () -> "The number of weights %d must match the number of source sets %d".formatted(weights.size(), + sets.length)); if (ClusterSlotHashUtil.isSameSlotForAllKeys(sets)) { @@ -951,8 +952,8 @@ public Long zInterStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(destKey, "Destination key must not be null"); Assert.notNull(sets, "Source sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); @@ -1008,8 +1009,9 @@ public Set zUnionWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + () -> "The number of weights %d must match the number of source sets %d".formatted(weights.size(), + sets.length)); if (ClusterSlotHashUtil.isSameSlotForAllKeys(sets)) { @@ -1052,8 +1054,8 @@ public Long zUnionStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(destKey, "Destination key must not be null"); Assert.notNull(sets, "Source sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java index 840f49dd04..5888498d2b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,6 +15,21 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.BuilderFactory; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; +import redis.clients.jedis.Transaction; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.commands.ServerCommands; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.util.Pool; + import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -26,31 +41,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.FallbackExceptionTranslationStrategy; import org.springframework.data.redis.RedisSystemException; -import org.springframework.data.redis.connection.AbstractRedisConnection; -import org.springframework.data.redis.connection.FutureResult; -import org.springframework.data.redis.connection.MessageListener; -import org.springframework.data.redis.connection.RedisCommands; -import org.springframework.data.redis.connection.RedisGeoCommands; -import org.springframework.data.redis.connection.RedisHashCommands; -import org.springframework.data.redis.connection.RedisHyperLogLogCommands; -import org.springframework.data.redis.connection.RedisKeyCommands; -import org.springframework.data.redis.connection.RedisListCommands; -import org.springframework.data.redis.connection.RedisNode; -import org.springframework.data.redis.connection.RedisPipelineException; -import org.springframework.data.redis.connection.RedisScriptingCommands; -import org.springframework.data.redis.connection.RedisServerCommands; -import org.springframework.data.redis.connection.RedisSetCommands; -import org.springframework.data.redis.connection.RedisStreamCommands; -import org.springframework.data.redis.connection.RedisStringCommands; -import org.springframework.data.redis.connection.RedisSubscribedConnectionException; -import org.springframework.data.redis.connection.RedisZSetCommands; -import org.springframework.data.redis.connection.Subscription; +import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.convert.TransactionResultConverter; import org.springframework.data.redis.connection.jedis.JedisInvoker.ResponseCommands; import org.springframework.data.redis.connection.jedis.JedisResult.JedisResultBuilder; @@ -59,21 +57,6 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import redis.clients.jedis.BuilderFactory; -import redis.clients.jedis.CommandArguments; -import redis.clients.jedis.CommandObject; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisClientConfig; -import redis.clients.jedis.Pipeline; -import redis.clients.jedis.Response; -import redis.clients.jedis.Transaction; -import redis.clients.jedis.commands.ProtocolCommand; -import redis.clients.jedis.commands.ServerCommands; -import redis.clients.jedis.exceptions.JedisDataException; -import redis.clients.jedis.util.Pool; - /** * {@code RedisConnection} implementation on top of Jedis library. *

    @@ -148,7 +131,7 @@ public JedisConnection(Jedis jedis) { } /** - * Constructs a new <{@link JedisConnection} backed by a Jedis {@link Pool}. + * Constructs a new {@link JedisConnection} backed by a Jedis {@link Pool}. * * @param jedis {@link Jedis} client. * @param pool {@link Pool} of Redis connections; can be null, if no pool is used. @@ -159,7 +142,7 @@ public JedisConnection(Jedis jedis, Pool pool, int dbIndex) { } /** - * Constructs a new <{@link JedisConnection} backed by a Jedis {@link Pool}. + * Constructs a new {@link JedisConnection} backed by a Jedis {@link Pool}. * * @param jedis {@link Jedis} client. * @param pool {@link Pool} of Redis connections; can be null, if no pool is used. @@ -172,7 +155,7 @@ protected JedisConnection(Jedis jedis, @Nullable Pool pool, int dbIndex, } /** - * Constructs a new <{@link JedisConnection} backed by a Jedis {@link Pool}. + * Constructs a new {@link JedisConnection} backed by a Jedis {@link Pool}. * * @param jedis {@link Jedis} client. * @param pool {@link Pool} of Redis connections; can be null, if no pool is used. diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index 4e83c271cf..01cb4badec 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -711,6 +711,8 @@ private JedisClientConfig createClientConfig(int database, @Nullable String user this.clientConfiguration.getSslParameters().ifPresent(builder::sslParameters); } + this.clientConfiguration.getCustomizer().ifPresent(customizer -> customizer.customize(builder)); + return builder.build(); } @@ -728,6 +730,12 @@ public void start() { if (getUsePool() && !isRedisClusterAware()) { this.pool = createPool(); + + try { + this.pool.preparePool(); + } catch (Exception ex) { + throw new PoolException("Could not prepare the pool", ex); + } } if (isRedisClusterAware()) { @@ -1003,7 +1011,7 @@ private Jedis getActiveSentinel() { return jedis; } } catch (Exception ex) { - log.warn(String.format("Ping failed for sentinel host: %s", node.getHost()), ex); + log.warn("Ping failed for sentinel host: %s".formatted(node.getHost()), ex); } finally { if (!success && jedis != null) { jedis.close(); @@ -1040,8 +1048,8 @@ private int getConnectTimeout() { private MutableJedisClientConfiguration getMutableConfiguration() { Assert.state(clientConfiguration instanceof MutableJedisClientConfiguration, - () -> String.format("Client configuration must be instance of MutableJedisClientConfiguration but is %s", - ClassUtils.getShortName(clientConfiguration.getClass()))); + () -> "Client configuration must be instance of MutableJedisClientConfiguration but is %s" + .formatted(ClassUtils.getShortName(clientConfiguration.getClass()))); return (MutableJedisClientConfiguration) clientConfiguration; } @@ -1056,10 +1064,10 @@ private void assertInitialized() { switch (current) { case CREATED, STOPPED -> throw new IllegalStateException( - String.format("JedisConnectionFactory has been %s. Use start() to initialize it", current)); + "JedisConnectionFactory has been %s. Use start() to initialize it".formatted(current)); case DESTROYED -> throw new IllegalStateException("JedisConnectionFactory was destroyed and cannot be used anymore"); - default -> throw new IllegalStateException(String.format("JedisConnectionFactory is %s", current)); + default -> throw new IllegalStateException("JedisConnectionFactory is %s".formatted(current)); } } @@ -1087,6 +1095,11 @@ public static JedisClientConfiguration create(GenericObjectPoolConfig jedisPoolC return configuration; } + @Override + public Optional getCustomizer() { + return Optional.empty(); + } + @Override public boolean isUseSsl() { return useSsl; diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 7a4c15613c..6be56d07b0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -472,7 +472,7 @@ private static byte[] boundaryToBytes(org.springframework.data.domain.Range.Boun } else if (theValue instanceof String string) { value = toBytes(string); } else { - throw new IllegalArgumentException(String.format("Cannot convert %s to binary format", boundary.getValue())); + throw new IllegalArgumentException("Cannot convert %s to binary format".formatted(boundary.getValue())); } ByteBuffer buffer = ByteBuffer.allocate(prefix.length + value.length); @@ -760,9 +760,8 @@ private static GeoSearchParam getGeoSearchParam(GeoShape predicate, GeoSearchPar return param; } - if (predicate instanceof BoxShape) { + if (predicate instanceof BoxShape boxPredicate) { - BoxShape boxPredicate = (BoxShape) predicate; BoundingBox boundingBox = boxPredicate.getBoundingBox(); param.byBox(boundingBox.getWidth().getValue(), boundingBox.getHeight().getValue(), @@ -771,7 +770,7 @@ private static GeoSearchParam getGeoSearchParam(GeoShape predicate, GeoSearchPar return param; } - throw new IllegalArgumentException(String.format("Cannot convert %s to Jedis GeoSearchParam", predicate)); + throw new IllegalArgumentException("Cannot convert %s to Jedis GeoSearchParam".formatted(predicate)); } private static void configureGeoReference(GeoReference reference, GeoSearchParam param) { @@ -782,14 +781,13 @@ private static void configureGeoReference(GeoReference reference, GeoSea return; } - if (reference instanceof GeoReference.GeoCoordinateReference) { + if (reference instanceof GeoReference.GeoCoordinateReference coordinates) { - GeoReference.GeoCoordinateReference coordinates = (GeoReference.GeoCoordinateReference) reference; param.fromLonLat(coordinates.getLongitude(), coordinates.getLatitude()); return; } - throw new IllegalArgumentException(String.format("Cannot extract Geo Reference from %s", reference)); + throw new IllegalArgumentException("Cannot extract Geo Reference from %s".formatted(reference)); } /** diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java index 743565b76a..50d594afa7 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/jedis/JedisGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java index a5f806974d..e034cefb0b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index be2cf8bb90..33189c27b4 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +16,7 @@ package org.springframework.data.redis.connection.jedis; import redis.clients.jedis.Jedis; +import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.commands.PipelineBinaryCommands; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; @@ -25,8 +26,10 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.Cursor; @@ -43,6 +46,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Tihomir Mateev * @since 2.0 */ class JedisHashCommands implements RedisHashCommands { @@ -150,7 +154,8 @@ public List> hRandFieldWithValues(byte[] key, long count) List> convertedMapEntryList = new ArrayList<>(mapEntryList.size()); - mapEntryList.forEach(entry -> convertedMapEntryList.add(Converters.entryOf(entry.getKey(), entry.getValue()))); + mapEntryList + .forEach(entry -> convertedMapEntryList.add(Converters.entryOf(entry.getKey(), entry.getValue()))); return convertedMapEntryList; @@ -237,8 +242,8 @@ protected ScanIteration> doScan(byte[] key, CursorId curso ScanParams params = JedisConverters.toScanParams(options); - ScanResult> result = connection.getJedis().hscan(key, - JedisConverters.toBytes(cursorId), params); + ScanResult> result = connection.getJedis().hscan(key, JedisConverters.toBytes(cursorId), + params); return new ScanIteration<>(CursorId.of(result.getCursor()), result.getResult()); } @@ -250,6 +255,74 @@ protected void doClose() { }.open(); } + @Override + public List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, option, fields); + } + + @Override + public List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, option, fields); + } + + @Override + public List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, option, fields); + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields) { + + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, + fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, + fields); + } + + @Override + public List hPersist(byte[] key, byte[]... fields) { + return connection.invoke().just(Jedis::hpersist, PipelineBinaryCommands::hpersist, key, fields); + } + + @Override + public List hTtl(byte[] key, byte[]... fields) { + return connection.invoke().just(Jedis::httl, PipelineBinaryCommands::httl, key, fields); + } + + @Override + public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + return connection.invoke().fromMany(Jedis::httl, PipelineBinaryCommands::httl, key, fields) + .toList(Converters.secondsToTimeUnit(timeUnit)); + } + + @Override + public List hpTtl(byte[] key, byte[]... fields) { + return connection.invoke().just(Jedis::hpttl, PipelineBinaryCommands::hpttl, key, fields); + } + @Nullable @Override public Long hStrLen(byte[] key, byte[] field) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHyperLogLogCommands.java index 64e7dc92e1..25393cf2af 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisInvoker.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisInvoker.java index 8d7a1cf2d3..586a35eca2 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisInvoker.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/jedis/JedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java index 58fc4e2408..9d4465b410 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.commands.JedisBinaryCommands; import redis.clients.jedis.commands.PipelineBinaryCommands; import redis.clients.jedis.params.RestoreParams; @@ -30,6 +31,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisKeyCommands; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.ValueEncoding; @@ -206,43 +208,69 @@ public Boolean renameNX(byte[] sourceKey, byte[] targetKey) { } @Override - public Boolean expire(byte[] key, long seconds) { + public Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); if (seconds > Integer.MAX_VALUE) { - return pExpire(key, TimeUnit.SECONDS.toMillis(seconds)); + return pExpire(key, TimeUnit.SECONDS.toMillis(seconds), condition); } - return connection.invoke().from(JedisBinaryCommands::expire, PipelineBinaryCommands::expire, key, seconds) + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().from(JedisBinaryCommands::expire, PipelineBinaryCommands::expire, key, seconds) + .get(JedisConverters.longToBoolean()); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().from(JedisBinaryCommands::expire, PipelineBinaryCommands::expire, key, seconds, option) .get(JedisConverters.longToBoolean()); } @Override - public Boolean pExpire(byte[] key, long millis) { + public Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().from(JedisBinaryCommands::pexpire, PipelineBinaryCommands::pexpire, key, millis) + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().from(JedisBinaryCommands::pexpire, PipelineBinaryCommands::pexpire, key, millis) + .get(JedisConverters.longToBoolean()); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().from(JedisBinaryCommands::pexpire, PipelineBinaryCommands::pexpire, key, millis, option) .get(JedisConverters.longToBoolean()); } @Override - public Boolean expireAt(byte[] key, long unixTime) { + public Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().from(JedisBinaryCommands::expireAt, PipelineBinaryCommands::expireAt, key, unixTime) + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke().from(JedisBinaryCommands::expireAt, PipelineBinaryCommands::expireAt, key, unixTime) + .get(JedisConverters.longToBoolean()); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke() + .from(JedisBinaryCommands::expireAt, PipelineBinaryCommands::expireAt, key, unixTime, option) .get(JedisConverters.longToBoolean()); } @Override - public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + public Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); + if (condition == ExpirationOptions.Condition.ALWAYS) { + return connection.invoke() + .from(JedisBinaryCommands::pexpireAt, PipelineBinaryCommands::pexpireAt, key, unixTimeInMillis) + .get(JedisConverters.longToBoolean()); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); return connection.invoke() - .from(JedisBinaryCommands::pexpireAt, PipelineBinaryCommands::pexpireAt, key, unixTimeInMillis) + .from(JedisBinaryCommands::pexpireAt, PipelineBinaryCommands::pexpireAt, key, unixTimeInMillis, option) .get(JedisConverters.longToBoolean()); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java index fc3e54259a..a7bb1dd619 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisMessageListener.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisMessageListener.java index e44685e32b..2092efeaf1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisMessageListener.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/jedis/JedisResult.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisResult.java index ca68b02d43..16b1483103 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisResult.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisScriptReturnConverter.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptReturnConverter.java index d7a2223a18..0398623c5e 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptReturnConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptReturnConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/jedis/JedisScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java index 13eb3178b4..5f7b76d592 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,20 +16,22 @@ package org.springframework.data.redis.connection.jedis; import redis.clients.jedis.Jedis; +import redis.clients.jedis.commands.ScriptingKeyPipelineBinaryCommands; import java.util.List; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisScriptingCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.util.Assert; /** * @author Mark Paluch + * @author Ivan Kripakov * @since 2.0 */ class JedisScriptingCommands implements RedisScriptingCommands { + private static final byte[] SAMPLE_KEY = new byte[0]; private final JedisConnection connection; JedisScriptingCommands(JedisConnection connection) { @@ -38,27 +40,21 @@ class JedisScriptingCommands implements RedisScriptingCommands { @Override public void scriptFlush() { - - assertDirectMode(); - - connection.invoke().just(Jedis::scriptFlush); + connection.invoke().just(Jedis::scriptFlush, it -> it.scriptFlush(SAMPLE_KEY)); } @Override public void scriptKill() { - - assertDirectMode(); - - connection.invoke().just(Jedis::scriptKill); + connection.invoke().just(Jedis::scriptKill, it -> it.scriptKill(SAMPLE_KEY)); } @Override public String scriptLoad(byte[] script) { Assert.notNull(script, "Script must not be null"); - assertDirectMode(); - return connection.invoke().from(it -> it.scriptLoad(script)).get(JedisConverters::toString); + return connection.invoke().from(it -> it.scriptLoad(script), it -> it.scriptLoad(script, SAMPLE_KEY)) + .get(JedisConverters::toString); } @Override @@ -66,9 +62,13 @@ public List scriptExists(String... scriptSha1) { Assert.notNull(scriptSha1, "Script digests must not be null"); Assert.noNullElements(scriptSha1, "Script digests must not contain null elements"); - assertDirectMode(); - return connection.invoke().just(it -> it.scriptExists(scriptSha1)); + byte[][] sha1 = new byte[scriptSha1.length][]; + for (int i = 0; i < scriptSha1.length; i++) { + sha1[i] = JedisConverters.toBytes(scriptSha1[i]); + } + + return connection.invoke().just(it -> it.scriptExists(scriptSha1), it -> it.scriptExists(SAMPLE_KEY, sha1)); } @Override @@ -76,11 +76,11 @@ public List scriptExists(String... scriptSha1) { public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { Assert.notNull(script, "Script must not be null"); - assertDirectMode(); JedisScriptReturnConverter converter = new JedisScriptReturnConverter(returnType); - return (T) connection.invoke().from(it -> it.eval(script, numKeys, keysAndArgs)).getOrElse(converter, - () -> converter.convert(null)); + return (T) connection.invoke() + .from(Jedis::eval, ScriptingKeyPipelineBinaryCommands::eval, script, numKeys, keysAndArgs) + .getOrElse(converter, () -> converter.convert(null)); } @Override @@ -93,17 +93,12 @@ public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte public T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { Assert.notNull(scriptSha, "Script digest must not be null"); - assertDirectMode(); JedisScriptReturnConverter converter = new JedisScriptReturnConverter(returnType); - return (T) connection.invoke().from(it -> it.evalsha(scriptSha, numKeys, keysAndArgs)).getOrElse(converter, - () -> converter.convert(null)); - } - - private void assertDirectMode() { - if (connection.isQueueing() || connection.isPipelined()) { - throw new InvalidDataAccessApiUsageException("Scripting commands not supported in pipelining/transaction mode"); - } + return (T) connection.invoke() + .from(Jedis::evalsha, ScriptingKeyPipelineBinaryCommands::evalsha, scriptSha, numKeys, keysAndArgs) + .getOrElse(converter, () -> converter.convert(null) + ); } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java index 8af29b6792..15dcb28db0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/jedis/JedisServerCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisServerCommands.java index bab0522591..a7e0feca2b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -161,7 +161,7 @@ public void killClient(String host, int port) { Assert.hasText(host, "Host for 'CLIENT KILL' must not be 'null' or 'empty'"); - connection.invokeStatus().just(it -> it.clientKill(String.format("%s:%s", host, port))); + connection.invokeStatus().just(it -> it.clientKill("%s:%s".formatted(host, port))); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSetCommands.java index c9ed8280de..c2521b82db 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStreamCommands.java index c3ecbe8255..483b299c9c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/jedis/JedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java index 889e87b102..392591e36a 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -35,6 +35,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author dengliming + * @author Marcin Grzejszczak * @since 2.0 */ class JedisStringCommands implements RedisStringCommands { @@ -116,6 +117,21 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op .getOrElse(Converters.stringToBooleanConverter(), () -> false); } + @Override + @Nullable + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + SetParams params = JedisConverters.toSetCommandExPxArgument(expiration, + JedisConverters.toSetCommandNxXxArgument(option)); + + return connection.invoke().just(Jedis::setGet, PipelineBinaryCommands::setGet, key, value, params); + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSubscription.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSubscription.java index 46a9b49668..6d34e4628d 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSubscription.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSubscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/jedis/JedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java index 749e198bcb..e3cef50537 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -474,8 +474,8 @@ public Set zInterWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights (%d) must match the number of source sets (%d)".formatted(weights.size(), sets.length)); return connection.invoke().fromMany(Jedis::zinterWithScores, PipelineBinaryCommands::zinterWithScores, toZParams(aggregate, weights), sets).toSet(JedisConverters::toTuple); @@ -487,8 +487,8 @@ public Long zInterStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(destKey, "Destination key must not be null"); Assert.notNull(sets, "Source sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZParams zparams = toZParams(aggregate, weights); @@ -528,8 +528,8 @@ public Set zUnionWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); return connection.invoke().fromMany(Jedis::zunionWithScores, PipelineBinaryCommands::zunionWithScores, toZParams(aggregate, weights), sets).toSet(JedisConverters::toTuple); @@ -542,8 +542,8 @@ public Long zUnionStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(sets, "Source sets must not be null"); Assert.notNull(weights, "Weights must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZParams zparams = toZParams(aggregate, weights); diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/StreamConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/StreamConverters.java index 60d16a1f4a..a68eef451a 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/StreamConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/StreamConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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. @@ -96,9 +96,8 @@ static List mapToList(Map map) { if (v instanceof StreamEntryID) { sources.add(v.toString()); - } else if (v instanceof StreamEntry) { + } else if (v instanceof StreamEntry streamEntry) { List entries = new ArrayList<>(2); - StreamEntry streamEntry = (StreamEntry) v; entries.add(streamEntry.getID().toString()); entries.add(streamEntry.getFields()); sources.add(entries); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/ClusterConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/ClusterConnectionProvider.java index 0b2061f5a7..60564519fa 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/ClusterConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/ClusterConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -120,9 +120,8 @@ private Optional getReadFrom() { }); } - String message = String.format("Connection type %s not supported", connectionType); - - return LettuceFutureUtils.failed(new InvalidDataAccessApiUsageException(message)); + return LettuceFutureUtils + .failed(new InvalidDataAccessApiUsageException("Connection type %s not supported".formatted(connectionType))); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java index c166000bca..787e278d95 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.resource.ClientResources; import java.time.Duration; @@ -30,12 +31,13 @@ * @author Mark Paluch * @author Christoph Strobl * @author Yanming Zhou + * @author Zhian Chen * @since 2.0 */ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration { private final boolean useSsl; - private final boolean verifyPeer; + private final SslVerifyMode verifyMode; private final boolean startTls; private final Optional clientResources; private final Optional clientOptions; @@ -46,13 +48,13 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration { private final Duration shutdownTimeout; private final Duration shutdownQuietPeriod; - DefaultLettuceClientConfiguration(boolean useSsl, boolean verifyPeer, boolean startTls, + DefaultLettuceClientConfiguration(boolean useSsl, SslVerifyMode verifyMode, boolean startTls, @Nullable ClientResources clientResources, @Nullable ClientOptions clientOptions, @Nullable String clientName, @Nullable ReadFrom readFrom, @Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory, - Duration timeout, Duration shutdownTimeout, @Nullable Duration shutdownQuietPeriod) { + Duration timeout, Duration shutdownTimeout, Duration shutdownQuietPeriod) { this.useSsl = useSsl; - this.verifyPeer = verifyPeer; + this.verifyMode = verifyMode; this.startTls = startTls; this.clientResources = Optional.ofNullable(clientResources); this.clientOptions = Optional.ofNullable(clientOptions); @@ -61,7 +63,7 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration { this.redisCredentialsProviderFactory = Optional.ofNullable(redisCredentialsProviderFactory); this.timeout = timeout; this.shutdownTimeout = shutdownTimeout; - this.shutdownQuietPeriod = shutdownQuietPeriod != null ? shutdownQuietPeriod : shutdownTimeout; + this.shutdownQuietPeriod = shutdownQuietPeriod; } @Override @@ -71,7 +73,12 @@ public boolean isUseSsl() { @Override public boolean isVerifyPeer() { - return verifyPeer; + return verifyMode != SslVerifyMode.NONE; + } + + @Override + public SslVerifyMode getVerifyMode() { + return verifyMode; } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolingClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolingClientConfiguration.java index fac5f8ec20..7d8cfa4d78 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolingClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolingClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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 io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; +import io.lettuce.core.SslVerifyMode; +import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.resource.ClientResources; import java.time.Duration; @@ -30,15 +32,16 @@ * @author Mark Paluch * @author Christoph Strobl * @author Yanming Zhou + * @author Zhian Chen * @since 2.0 */ class DefaultLettucePoolingClientConfiguration implements LettucePoolingClientConfiguration { private final LettuceClientConfiguration clientConfiguration; - private final GenericObjectPoolConfig poolConfig; + private final GenericObjectPoolConfig> poolConfig; DefaultLettucePoolingClientConfiguration(LettuceClientConfiguration clientConfiguration, - GenericObjectPoolConfig poolConfig) { + GenericObjectPoolConfig> poolConfig) { this.clientConfiguration = clientConfiguration; this.poolConfig = poolConfig; @@ -50,10 +53,16 @@ public boolean isUseSsl() { } @Override + @Deprecated public boolean isVerifyPeer() { return clientConfiguration.isVerifyPeer(); } + @Override + public SslVerifyMode getVerifyMode() { + return clientConfiguration.getVerifyMode(); + } + @Override public boolean isStartTls() { return clientConfiguration.isStartTls(); @@ -100,7 +109,7 @@ public Duration getShutdownQuietPeriod() { } @Override - public GenericObjectPoolConfig getPoolConfig() { + public GenericObjectPoolConfig> getPoolConfig() { return poolConfig; } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceByteBufferPubSubListenerWrapper.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceByteBufferPubSubListenerWrapper.java index d76cc2aee1..c4bdc997a2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceByteBufferPubSubListenerWrapper.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceByteBufferPubSubListenerWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/lettuce/LettuceClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java index 6372226843..4cc752877d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -18,6 +18,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; import io.lettuce.core.RedisURI; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.TimeoutOptions; import io.lettuce.core.resource.ClientResources; @@ -50,6 +51,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Yanming Zhou + * @author Zhian Chen * @since 2.0 * @see org.springframework.data.redis.connection.RedisStandaloneConfiguration * @see org.springframework.data.redis.connection.RedisSentinelConfiguration @@ -64,9 +66,17 @@ public interface LettuceClientConfiguration { /** * @return {@literal true} to verify peers when using {@link #isUseSsl() SSL}. + * @deprecated since 3.4, use {@link #getVerifyMode()} for how peer verification is configured. */ + @Deprecated(since = "3.4") boolean isVerifyPeer(); + /** + * @return the {@link io.lettuce.core.SslVerifyMode}. + * @since 3.4 + */ + SslVerifyMode getVerifyMode(); + /** * @return {@literal true} to use Start TLS ({@code true} if the first write request shouldn't be encrypted). */ @@ -166,7 +176,7 @@ static LettuceClientConfiguration defaultConfiguration() { class LettuceClientConfigurationBuilder { boolean useSsl; - boolean verifyPeer = true; + SslVerifyMode verifyMode = SslVerifyMode.FULL; boolean startTls; @Nullable ClientResources clientResources; ClientOptions clientOptions = ClientOptions.builder().timeoutOptions(TimeoutOptions.enabled()).build(); @@ -175,7 +185,7 @@ class LettuceClientConfigurationBuilder { @Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory; Duration timeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT); Duration shutdownTimeout = Duration.ofMillis(100); - @Nullable Duration shutdownQuietPeriod; + Duration shutdownQuietPeriod = Duration.ZERO; LettuceClientConfigurationBuilder() {} @@ -189,7 +199,7 @@ class LettuceClientConfigurationBuilder { public LettuceClientConfigurationBuilder apply(RedisURI redisUri) { this.useSsl = redisUri.isSsl(); - this.verifyPeer = redisUri.isVerifyPeer(); + this.verifyMode = redisUri.getVerifyMode(); this.startTls = redisUri.isStartTls(); if (!redisUri.getTimeout().equals(RedisURI.DEFAULT_TIMEOUT_DURATION)) { @@ -347,7 +357,7 @@ public LettuceClientConfigurationBuilder shutdownQuietPeriod(Duration shutdownQu */ public LettuceClientConfiguration build() { - return new DefaultLettuceClientConfiguration(useSsl, verifyPeer, startTls, clientResources, clientOptions, + return new DefaultLettuceClientConfiguration(useSsl, verifyMode, startTls, clientResources, clientOptions, clientName, readFrom, redisCredentialsProviderFactory, timeout, shutdownTimeout, shutdownQuietPeriod); } } @@ -357,7 +367,7 @@ public LettuceClientConfiguration build() { */ class LettuceSslClientConfigurationBuilder { - private LettuceClientConfigurationBuilder delegate; + private final LettuceClientConfigurationBuilder delegate; LettuceSslClientConfigurationBuilder(LettuceClientConfigurationBuilder delegate) { @@ -366,16 +376,28 @@ class LettuceSslClientConfigurationBuilder { } /** - * Disable peer verification. + * Configure peer verification. * * @return {@literal this} builder. + * @since 3.4 */ - public LettuceSslClientConfigurationBuilder disablePeerVerification() { + public LettuceSslClientConfigurationBuilder verifyPeer(SslVerifyMode verifyMode) { + + Assert.notNull(verifyMode, "SslVerifyMode must not be null"); - delegate.verifyPeer = false; + delegate.verifyMode = verifyMode; return this; } + /** + * Disable peer verification. + * + * @return {@literal this} builder. + */ + public LettuceSslClientConfigurationBuilder disablePeerVerification() { + return verifyPeer(SslVerifyMode.NONE); + } + /** * Enable Start TLS to send the first bytes unencrypted. * diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java index 3756e9f8b4..41a7cb94e1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -216,10 +216,8 @@ private RedisClusterClient getClient() { return (RedisClusterClient) redisClientProvider.getRedisClient(); } - String message = String.format("Connection provider %s does not implement RedisClientProvider", - connectionProvider.getClass().getName()); - - throw new IllegalStateException(message); + throw new IllegalStateException("Connection provider %s does not implement RedisClientProvider" + .formatted(connectionProvider.getClass().getName())); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterGeoCommands.java index d220ed3cb4..96196ca6a6 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHashCommands.java index 29f36d29e7..aa5db78bac 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHyperLogLogCommands.java index d8215c338f..f0fe06ccdb 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java index ae3d3b2e49..09eabc950e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterListCommands.java index f7905764e5..9c27d41b64 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterServerCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterServerCommands.java index 6d2e66cda0..d17f801899 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterSetCommands.java index 3a346141c2..2ff5bb3a84 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterStringCommands.java index 4b6ae60f04..986569e55d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterTopologyProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterTopologyProvider.java index 5176bf68e1..fb789b056c 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterTopologyProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterTopologyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterZSetCommands.java index fa80eeda87..a99532618e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index c3ecbde730..fe646b3a1b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -328,7 +328,7 @@ public Object execute(String command, byte[]... args) { @SuppressWarnings({ "rawtypes", "unchecked" }) public Object execute(String command, @Nullable CommandOutput commandOutputTypeHint, byte[]... args) { - Assert.hasText(command, () -> String.format("A valid command [%s] needs to be specified", command)); + Assert.hasText(command, () -> "A valid command [%s] needs to be specified".formatted(command)); ProtocolKeyword commandType = getCommandType(command.trim().toUpperCase()); @@ -512,7 +512,7 @@ private void reset() { if (this.asyncDedicatedConnection != null) { try { if (customizedDatabaseIndex()) { - potentiallySelectDatabase(this.defaultDbIndex); + potentiallySelectDatabase(this.asyncDedicatedConnection, this.defaultDbIndex); } this.connectionProvider.release(this.asyncDedicatedConnection); this.asyncDedicatedConnection = null; @@ -937,9 +937,7 @@ RedisClusterCommands getDedicatedConnection() { return statefulClusterConnection.sync(); } - String message = String.format("%s is not a supported connection type", connection.getClass().getName()); - - throw new IllegalStateException(message); + throw new IllegalStateException("%s is not a supported connection type".formatted(connection.getClass().getName())); } protected RedisClusterAsyncCommands getAsyncDedicatedConnection() { @@ -953,25 +951,17 @@ protected RedisClusterAsyncCommands getAsyncDedicatedConnection( if (connection instanceof StatefulRedisConnection statefulConnection) { return statefulConnection.async(); } + if (asyncDedicatedConnection instanceof StatefulRedisClusterConnection statefulClusterConnection) { return statefulClusterConnection.async(); } - String message = String.format("%s is not a supported connection type", connection.getClass().getName()); - - throw new IllegalStateException(message); + throw new IllegalStateException("%s is not a supported connection type".formatted(connection.getClass().getName())); } @SuppressWarnings("unchecked") protected StatefulConnection doGetAsyncDedicatedConnection() { - - StatefulConnection connection = getConnectionProvider().getConnection(StatefulConnection.class); - - if (customizedDatabaseIndex()) { - potentiallySelectDatabase(this.dbIndex); - } - - return connection; + return getConnectionProvider().getConnection(StatefulConnection.class); } @Override @@ -1065,9 +1055,9 @@ private boolean customizedDatabaseIndex() { return defaultDbIndex != dbIndex; } - private void potentiallySelectDatabase(int dbIndex) { + private static void potentiallySelectDatabase(StatefulConnection connection, int dbIndex) { - if (asyncDedicatedConnection instanceof StatefulRedisConnection statefulConnection) { + if (connection instanceof StatefulRedisConnection statefulConnection) { statefulConnection.sync().select(dbIndex); } } @@ -1085,14 +1075,13 @@ private void validateCommandIfRunningInTransactionMode(ProtocolKeyword cmd, byte private void validateCommand(ProtocolKeyword command, @Nullable byte[]... args) { - RedisCommand redisCommand = RedisCommand.failsafeCommandLookup(command.name()); + RedisCommand redisCommand = RedisCommand.failsafeCommandLookup(command.toString()); if (!RedisCommand.UNKNOWN.equals(redisCommand) && redisCommand.requiresArguments()) { try { redisCommand.validateArgumentCount(args != null ? args.length : 0); } catch (IllegalArgumentException ex) { - String message = String.format("Validation failed for %s command", command); - throw new InvalidDataAccessApiUsageException(message, ex); + throw new InvalidDataAccessApiUsageException("Validation failed for %s command".formatted(command), ex); } } } @@ -1171,6 +1160,14 @@ static class TypeHints { COMMAND_OUTPUT_TYPE_MAPPING.put(PFMERGE, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(PFADD, IntegerOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPERSIST, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HTTL, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPTTL, IntegerListOutput.class); + // DOUBLE COMMAND_OUTPUT_TYPE_MAPPING.put(HINCRBYFLOAT, DoubleOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(INCRBYFLOAT, DoubleOutput.class); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index c406faaa1e..befdfffe50 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -24,6 +24,7 @@ import io.lettuce.core.RedisConnectionException; import io.lettuce.core.RedisCredentialsProvider; import io.lettuce.core.RedisURI; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.ClusterClientOptions; @@ -63,7 +64,6 @@ import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration; import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex; import org.springframework.data.redis.connection.RedisConfiguration.WithPassword; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -115,6 +115,7 @@ * @author Andrea Como * @author Chris Bono * @author John Blum + * @author Zhian Chen */ public class LettuceConnectionFactory implements RedisConnectionFactory, ReactiveRedisConnectionFactory, InitializingBean, DisposableBean, SmartLifecycle { @@ -473,7 +474,9 @@ public void setUseSsl(boolean useSsl) { * Returns whether to verify certificate validity/hostname check when SSL is used. * * @return whether to verify peers when using SSL. + * @deprecated since 3.4, use {@link LettuceClientConfiguration#getVerifyMode()} instead. */ + @Deprecated(since = "3.4") public boolean isVerifyPeer() { return clientConfiguration.isVerifyPeer(); } @@ -562,7 +565,7 @@ public void setShareNativeConnection(boolean shareNativeConnection) { * connection factory configuration. Eager initialization also prevents blocking connect while using reactive API and * is recommended for reactive API usage. * - * @return {@link true} if the shared connection is initialized upon {@link #start()}. + * @return {@literal true} if the shared connection is initialized upon {@link #start()}. * @since 2.2 * @see #start() */ @@ -666,8 +669,11 @@ public AbstractRedisClient getNativeClient() { */ public AbstractRedisClient getRequiredNativeClient() { - return RedisAssertions.requireState(getNativeClient(), - "Client not yet initialized; Did you forget to call initialize the bean"); + AbstractRedisClient client = getNativeClient(); + + Assert.state(client != null, "Client not yet initialized; Did you forget to call initialize the bean"); + + return client; } @Nullable @@ -1233,7 +1239,7 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { } /** - * @return the shared connection using {@literal byte[]} encoding for imperative API use. {@literal null} if + * @return the shared connection using {@code byte[]} encoding for imperative API use. {@literal null} if * {@link #getShareNativeConnection() connection sharing} is disabled or when connected to Redis Cluster. */ @Nullable @@ -1245,7 +1251,7 @@ protected StatefulRedisConnection getSharedConnection() { } /** - * @return the shared cluster connection using {@literal byte[]} encoding for imperative API use. {@literal null} if + * @return the shared cluster connection using {@code byte[]} encoding for imperative API use. {@literal null} if * {@link #getShareNativeConnection() connection sharing} is disabled or when connected to Redis * Standalone/Sentinel/Master-Replica. * @since 2.5.7 @@ -1360,7 +1366,7 @@ private RedisURI getSentinelRedisURI() { this.clientConfiguration.getClientName().ifPresent(it::setClientName); it.setSsl(this.clientConfiguration.isUseSsl()); - it.setVerifyPeer(this.clientConfiguration.isVerifyPeer()); + it.setVerifyPeer(this.clientConfiguration.getVerifyMode()); it.setStartTls(this.clientConfiguration.isStartTls()); it.setTimeout(this.clientConfiguration.getCommandTimeout()); }); @@ -1440,10 +1446,10 @@ private void assertStarted() { switch (current) { case CREATED, STOPPED -> throw new IllegalStateException( - String.format("LettuceConnectionFactory has been %s. Use start() to initialize it", current)); + "LettuceConnectionFactory has been %s. Use start() to initialize it".formatted(current)); case DESTROYED -> throw new IllegalStateException("LettuceConnectionFactory was destroyed and cannot be used anymore"); - default -> throw new IllegalStateException(String.format("LettuceConnectionFactory is %s", current)); + default -> throw new IllegalStateException("LettuceConnectionFactory is %s".formatted(current)); } } @@ -1463,7 +1469,7 @@ private RedisURI createRedisURIAndApplySettings(String host, int port) { builder.withDatabase(getDatabase()); builder.withSsl(clientConfiguration.isUseSsl()); - builder.withVerifyPeer(clientConfiguration.isVerifyPeer()); + builder.withVerifyPeer(clientConfiguration.getVerifyMode()); builder.withStartTls(clientConfiguration.isStartTls()); builder.withTimeout(clientConfiguration.getCommandTimeout()); @@ -1496,8 +1502,8 @@ private RedisURI.Builder applyAuthentication(RedisURI.Builder builder) { private MutableLettuceClientConfiguration getMutableConfiguration() { Assert.state(clientConfiguration instanceof MutableLettuceClientConfiguration, - () -> String.format("Client configuration must be instance of MutableLettuceClientConfiguration but is %s", - ClassUtils.getShortName(clientConfiguration.getClass()))); + () -> "Client configuration must be instance of MutableLettuceClientConfiguration but is %s" + .formatted(ClassUtils.getShortName(clientConfiguration.getClass()))); return (MutableLettuceClientConfiguration) clientConfiguration; } @@ -1659,7 +1665,7 @@ void resetConnection() { static class MutableLettuceClientConfiguration implements LettuceClientConfiguration { private boolean useSsl; - private boolean verifyPeer = true; + private SslVerifyMode verifyMode = SslVerifyMode.FULL; private boolean startTls; private @Nullable ClientResources clientResources; @@ -1680,11 +1686,16 @@ void setUseSsl(boolean useSsl) { @Override public boolean isVerifyPeer() { - return verifyPeer; + return verifyMode != SslVerifyMode.NONE; + } + + @Override + public SslVerifyMode getVerifyMode() { + return verifyMode; } void setVerifyPeer(boolean verifyPeer) { - this.verifyPeer = verifyPeer; + this.verifyMode = verifyPeer ? SslVerifyMode.FULL : SslVerifyMode.NONE; } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionProvider.java index cdfcf5c980..ad352da67d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index d4aa2d9cc8..5548d2d1f6 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -79,6 +79,7 @@ * @author Chris Bono * @author Vikas Garg * @author John Blum + * @author Roman Osadchuk */ @SuppressWarnings("ConstantConditions") public abstract class LettuceConverters extends Converters { @@ -720,7 +721,7 @@ public static BitFieldArgs toBitFieldArgs(BitFieldSubCommands subCommands) { args = args.overflow(type); } - args = args.incrBy(bitFieldType, (int) subCommand.getOffset().getValue(), ((BitFieldIncrBy) subCommand).getValue()); + args = args.incrBy(bitFieldType, offset, ((BitFieldIncrBy) subCommand).getValue()); } } @@ -862,16 +863,15 @@ static GeoSearch.GeoPredicate toGeoPredicate(GeoShape predicate) { return GeoSearch.byRadius(radius.getValue(), toGeoArgsUnit(radius.getMetric())); } - if (predicate instanceof BoxShape) { + if (predicate instanceof BoxShape boxPredicate) { - BoxShape boxPredicate = (BoxShape) predicate; BoundingBox boundingBox = boxPredicate.getBoundingBox(); return GeoSearch.byBox(boundingBox.getWidth().getValue(), boundingBox.getHeight().getValue(), toGeoArgsUnit(boxPredicate.getMetric())); } - throw new IllegalArgumentException(String.format("Cannot convert %s to Lettuce GeoPredicate", predicate)); + throw new IllegalArgumentException("Cannot convert %s to Lettuce GeoPredicate".formatted(predicate)); } static GeoSearch.GeoRef toGeoRef(GeoReference reference) { @@ -880,14 +880,12 @@ static GeoSearch.GeoRef toGeoRef(GeoReference reference) { return GeoSearch.fromMember(((GeoMemberReference) reference).getMember()); } - if (reference instanceof GeoReference.GeoCoordinateReference) { - - GeoCoordinateReference coordinates = (GeoCoordinateReference) reference; + if (reference instanceof GeoCoordinateReference coordinates) { return GeoSearch.fromCoordinates(coordinates.getLongitude(), coordinates.getLatitude()); } - throw new IllegalArgumentException(String.format("Cannot convert %s to Lettuce GeoRef", reference)); + throw new IllegalArgumentException("Cannot convert %s to Lettuce GeoRef".formatted(reference)); } static FlushMode toFlushMode(@Nullable RedisServerCommands.FlushOption option) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceExceptionConverter.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceExceptionConverter.java index 1b0a27441c..773f0a7522 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceExceptionConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceExceptionConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/lettuce/LettuceFutureUtils.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceFutureUtils.java index f1f56e1b5c..556d37b8f7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceFutureUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceFutureUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -87,8 +87,8 @@ static T join(CompletionStage future) throws RuntimeException, Completion /** * Returns a {@link Function} that ignores {@link CompletionStage#exceptionally(Function) exceptional completion} by - * recovering to {@code null}. This allows to progress with a previously failed {@link CompletionStage} without regard - * to the actual success/exception state. + * recovering to {@literal null}. This allows to progress with a previously failed {@link CompletionStage} without + * regard to the actual success/exception state. * * @return */ diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java index f0ff6e2715..0cad933193 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 5125a82fb6..278671704b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,17 +15,21 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; import io.lettuce.core.api.async.RedisHashAsyncCommands; +import io.lettuce.core.protocol.CommandArgs; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.Cursor; @@ -35,10 +39,12 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl * @author Mark Paluch + * @author Tihomir Mateev * @since 2.0 */ class LettuceHashCommands implements RedisHashCommands { @@ -208,6 +214,51 @@ public Cursor> hScan(byte[] key, ScanOptions options) { return hScan(key, CursorId.initial(), options); } + @Override + public List hExpire(byte[] key, long seconds, ExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, getExpireArgs(condition), fields) + .toList(); + } + + @Override + public List hpExpire(byte[] key, long millis, ExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, getExpireArgs(condition), fields) + .toList(); + } + + @Override + public List hExpireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, getExpireArgs(condition), fields).toList(); + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition, + byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, getExpireArgs(condition), fields).toList(); + } + + @Override + public List hPersist(byte[] key, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hpersist, key, fields).toList(); + } + + @Override + public List hTtl(byte[] key, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::httl, key, fields).toList(); + } + + @Override + public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::httl, key, fields) + .toList(Converters.secondsToTimeUnit(timeUnit)); + } + + @Override + public List hpTtl(byte[] key, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hpttl, key, fields).toList(); + } /** * @param key @@ -263,4 +314,19 @@ private static Entry toEntry(KeyValue value) { return value.hasValue() ? Converters.entryOf(value.getKey(), value.getValue()) : null; } + private static ExpireArgs getExpireArgs(ExpirationOptions.Condition condition) { + + return new ExpireArgs() { + @Override + public void build(CommandArgs args) { + + if (ObjectUtils.nullSafeEquals(condition, ExpirationOptions.Condition.ALWAYS)) { + return; + } + + args.add(condition.name()); + } + }; + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHyperLogLogCommands.java index 12fbbd5be2..d62e3b9d5e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceInvoker.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceInvoker.java index 7661db4a73..451593fc19 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceInvoker.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/connection/lettuce/LettuceKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java index 415da2ccaa..78d4e7006e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,12 +16,14 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.CopyArgs; +import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyScanCursor; import io.lettuce.core.RestoreArgs; import io.lettuce.core.ScanArgs; import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.api.async.RedisKeyAsyncCommands; +import io.lettuce.core.protocol.CommandArgs; import java.time.Duration; import java.util.List; @@ -30,6 +32,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisKeyCommands; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.ValueEncoding; @@ -39,6 +42,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl @@ -192,35 +196,35 @@ public Boolean renameNX(byte[] sourceKey, byte[] targetKey) { } @Override - public Boolean expire(byte[] key, long seconds) { + public Boolean expire(byte[] key, long seconds, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().just(RedisKeyAsyncCommands::expire, key, seconds); + return connection.invoke().just(RedisKeyAsyncCommands::expire, key, seconds, getExpireArgs(condition)); } @Override - public Boolean pExpire(byte[] key, long millis) { + public Boolean pExpire(byte[] key, long millis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().just(RedisKeyAsyncCommands::pexpire, key, millis); + return connection.invoke().just(RedisKeyAsyncCommands::pexpire, key, millis, getExpireArgs(condition)); } @Override - public Boolean expireAt(byte[] key, long unixTime) { + public Boolean expireAt(byte[] key, long unixTime, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().just(RedisKeyAsyncCommands::expireat, key, unixTime); + return connection.invoke().just(RedisKeyAsyncCommands::expireat, key, unixTime, getExpireArgs(condition)); } @Override - public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + public Boolean pExpireAt(byte[] key, long unixTimeInMillis, ExpirationOptions.Condition condition) { Assert.notNull(key, "Key must not be null"); - return connection.invoke().just(RedisKeyAsyncCommands::pexpireat, key, unixTimeInMillis); + return connection.invoke().just(RedisKeyAsyncCommands::pexpireat, key, unixTimeInMillis, getExpireArgs(condition)); } @Override @@ -337,4 +341,19 @@ public Long refcount(byte[] key) { return connection.invoke().just(RedisKeyAsyncCommands::objectRefcount, key); } + + private static ExpireArgs getExpireArgs(ExpirationOptions.Condition condition) { + + return new ExpireArgs() { + @Override + public void build(CommandArgs args) { + + if (ObjectUtils.nullSafeEquals(condition, ExpirationOptions.Condition.ALWAYS)) { + return; + } + + args.add(condition.name()); + } + }; + } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java index cc1be60431..4f930633f2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceMessageListener.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceMessageListener.java index 2c4b021bbd..1451a2829d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceMessageListener.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/lettuce/LettucePoolingClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfiguration.java index c9fa6c242e..dd06a6d3b7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; +import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.resource.ClientResources; import java.time.Duration; @@ -39,7 +40,7 @@ public interface LettucePoolingClientConfiguration extends LettuceClientConfigur /** * @return the {@link GenericObjectPoolConfig}. Never {@literal null}. */ - GenericObjectPoolConfig getPoolConfig(); + GenericObjectPoolConfig> getPoolConfig(); /** * Creates a new {@link LettucePoolingClientConfigurationBuilder} to build {@link LettucePoolingClientConfiguration} @@ -91,7 +92,7 @@ static LettucePoolingClientConfiguration defaultConfiguration() { */ class LettucePoolingClientConfigurationBuilder extends LettuceClientConfigurationBuilder { - GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); LettucePoolingClientConfigurationBuilder() { super(); @@ -163,7 +164,8 @@ public LettucePoolingClientConfigurationBuilder clientName(String clientName) { * * @param poolConfig must not be {@literal null}. */ - public LettucePoolingClientConfigurationBuilder poolConfig(GenericObjectPoolConfig poolConfig) { + public LettucePoolingClientConfigurationBuilder poolConfig( + GenericObjectPoolConfig> poolConfig) { Assert.notNull(poolConfig, "PoolConfig must not be null"); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java index d82e88c3a0..086c9ce763 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -55,6 +55,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Asmir Mustafic * @since 2.0 * @see #getConnection(Class) */ @@ -63,7 +64,7 @@ class LettucePoolingConnectionProvider implements LettuceConnectionProvider, Red private static final Log log = LogFactory.getLog(LettucePoolingConnectionProvider.class); private final LettuceConnectionProvider connectionProvider; - private final GenericObjectPoolConfig poolConfig; + private final GenericObjectPoolConfig> poolConfig; private final Map, GenericObjectPool>> poolRef = new ConcurrentHashMap<>( 32); @@ -90,8 +91,17 @@ class LettucePoolingConnectionProvider implements LettuceConnectionProvider, Red public > T getConnection(Class connectionType) { GenericObjectPool> pool = pools.computeIfAbsent(connectionType, poolType -> { - return ConnectionPoolSupport.createGenericObjectPool(() -> connectionProvider.getConnection(connectionType), - poolConfig, false); + + GenericObjectPool> newPool = ConnectionPoolSupport + .createGenericObjectPool(() -> connectionProvider.getConnection(connectionType), poolConfig, false); + + try { + newPool.preparePool(); + } catch (Exception ex) { + throw new PoolException("Could not prepare the pool", ex); + } + + return newPool; }); try { @@ -135,9 +145,8 @@ public AbstractRedisClient getRedisClient() { return ((RedisClientProvider) connectionProvider).getRedisClient(); } - throw new IllegalStateException( - String.format("Underlying connection provider %s does not implement RedisClientProvider", - connectionProvider.getClass().getName())); + throw new IllegalStateException("Underlying connection provider %s does not implement RedisClientProvider" + .formatted(connectionProvider.getClass().getName())); } @Override @@ -165,9 +174,8 @@ public void release(StatefulConnection connection) { private void discardIfNecessary(StatefulConnection connection) { - if (connection instanceof StatefulRedisConnection) { + if (connection instanceof StatefulRedisConnection redisConnection) { - StatefulRedisConnection redisConnection = (StatefulRedisConnection) connection; if (redisConnection.isMulti()) { redisConnection.async().discard(); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java index c797927296..d682898458 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java index 9b5969d88d..70e5f200bc 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java index 999207aa75..788b00e0c1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java index 87c34658f8..ed7dd87242 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java index 2001abdf17..0d1f56c7b5 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java index d25f6c1c95..d7dde2e6ed 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterScriptingCommands.java index e4907f90ce..8f084c5783 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveClusterServerCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommands.java index fca3ee62de..e0f12524e2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java index be5790956f..85d6feb3c1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStreamCommands.java index 9de831682a..1de72bfbc1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java index b0d4e6a1b8..5c466dc9ab 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java index ccd8de9354..a4f1ad2288 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java index 4502f5eb07..9425570f4a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index 0837489840..e637296219 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,10 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyValue; import io.lettuce.core.ScanStream; +import io.lettuce.core.protocol.CommandArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,10 +27,11 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.reactivestreams.Publisher; - +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.ReactiveHashCommands; import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; @@ -38,6 +41,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl @@ -264,6 +268,94 @@ public Flux> hStrLen(Publisher> applyHashFieldExpiration( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); + + ByteBuffer[] fields = command.getFields().toArray(ByteBuffer[]::new); + + if (command.getExpiration().isPersistent()) { + return cmd.hpersist(command.getKey(), fields).map(value -> new NumericResponse<>(command, value)); + } + + ExpireArgs args = new ExpireArgs() { + + @Override + public void build(CommandArgs args) { + super.build(args); + if (ObjectUtils.nullSafeEquals(command.getOptions(), ExpirationOptions.none())) { + return; + } + + args.add(command.getOptions().getCondition().name()); + } + }; + + if (command.getExpiration().isUnixTimestamp()) { + + if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) { + return cmd + .hpexpireat(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args, fields) + .map(value -> new NumericResponse<>(command, value)); + } + return cmd.hexpireat(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args, fields) + .map(value -> new NumericResponse<>(command, value)); + } + + if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) { + return cmd.hpexpire(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args, fields) + .map(value -> new NumericResponse<>(command, value)); + } + + return cmd.hexpire(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args, fields) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + @Override + public Flux> hPersist(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); + + return cmd.hpersist(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + @Override + public Flux> hTtl(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); + + return cmd.httl(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + @Override + public Flux> hpTtl(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); + + return cmd.hpttl(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)) + .map(value -> new NumericResponse<>(command, value)); + })); + } + private static Map.Entry toEntry(KeyValue kv) { return new Entry() { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java index f4cd6e9186..f7db48edd9 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java index a7df677064..f4b7b8b916 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,10 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.CopyArgs; +import io.lettuce.core.ExpireArgs; import io.lettuce.core.ScanStream; import io.lettuce.core.api.reactive.RedisKeyReactiveCommands; +import io.lettuce.core.protocol.CommandArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,9 +27,12 @@ import java.time.Duration; import java.util.Collection; import java.util.List; +import java.util.concurrent.TimeUnit; import org.reactivestreams.Publisher; + import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.ReactiveKeyCommands; import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; @@ -38,10 +43,12 @@ import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding; import org.springframework.data.redis.core.ScanOptions; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl * @author Mark Paluch + * @author Dahye Anne Lee * @since 2.0 */ class LettuceReactiveKeyCommands implements ReactiveKeyCommands { @@ -206,27 +213,45 @@ public Flux, Long>> mUnlink(Publisher> expire(Publisher commands) { + public Flux> applyExpiration(Publisher commands) { return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getTimeout(), "Timeout must not be null"); - return cmd.expire(command.getKey(), command.getTimeout().getSeconds()) - .map(value -> new BooleanResponse<>(command, value)); - })); - } + if (command.getExpiration().isPersistent()) { + return cmd.persist(command.getKey()).map(value -> new BooleanResponse<>(command, value)); + } - @Override - public Flux> pExpire(Publisher commands) { + ExpireArgs args = new ExpireArgs() { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + @Override + public void build(CommandArgs args) { + super.build(args); + if (ObjectUtils.nullSafeEquals(command.getOptions(), ExpirationOptions.none())) { + return; + } - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getTimeout(), "Timeout must not be null"); + args.add(command.getOptions().getCondition().name()); + } + }; + + if (command.getExpiration().isUnixTimestamp()) { + + if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) { + return cmd.pexpireat(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args) + .map(value -> new BooleanResponse<>(command, value)); + } + return cmd.expireat(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args) + .map(value -> new BooleanResponse<>(command, value)); + } + + if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) { + return cmd.pexpire(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args) + .map(value -> new BooleanResponse<>(command, value)); + } - return cmd.pexpire(command.getKey(), command.getTimeout().toMillis()) + return cmd.expire(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args) .map(value -> new BooleanResponse<>(command, value)); })); } @@ -239,7 +264,7 @@ public Flux> expireAt(Publisher new BooleanResponse<>(command, value)); })); } @@ -252,7 +277,7 @@ public Flux> pExpireAt(Publisher new BooleanResponse<>(command, value)); })); } @@ -319,4 +344,30 @@ public Mono idletime(ByteBuffer key) { public Mono refcount(ByteBuffer key) { return connection.execute(cmd -> cmd.objectRefcount(key)).next(); } + + @Override + public Mono exists(List keys) { + + Assert.notNull(keys, "Key list must not be null"); + Assert.notEmpty(keys, "Key list must not be empty"); + + return connection.execute(cmd -> cmd.exists(keys.toArray(ByteBuffer[]::new))).next(); + } + + private static ExpireArgs getExpireArgs(ExpirationOptions options) { + + return new ExpireArgs() { + + @Override + public void build(CommandArgs args) { + super.build(args); + if (ObjectUtils.nullSafeEquals(options.getCondition(), ExpirationOptions.Condition.ALWAYS)) { + return; + } + + args.add(options.getCondition().name()); + } + }; + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java index 2721877f4c..77e6280491 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,7 +74,7 @@ public Flux> push(Publisher comm if (!command.getUpsert() && command.getValues().size() > 1) { throw new InvalidDataAccessApiUsageException( - String.format("%s PUSHX only allows one value", command.getDirection())); + "%s PUSHX only allows one value".formatted(command.getDirection())); } Mono pushResult; diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java index fd4b0de4f6..f05cff7134 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactivePubSubCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommands.java index 7e25074679..63fd654e10 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -233,7 +233,7 @@ static boolean deallocate(ByteBuffer buffer, Map targe @Override public String toString() { - return String.format("%s: Subscribers: %s", new String(raw), SUBSCRIBERS.get(this)); + return "%s: Subscribers: %s".formatted(new String(raw), SUBSCRIBERS.get(this)); } } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java index a688e94060..b66d0827a0 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java index b99c098096..6905356065 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java index c6d5686486..ec89c3f831 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveServerCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommands.java index f3aa9bd2af..9b8f22f54a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -154,7 +154,7 @@ public Mono killClient(String host, int port) { Assert.notNull(host, "Host must not be null or empty"); - return connection.execute(c -> c.clientKill(String.format("%s:%s", host, port))).next(); + return connection.execute(c -> c.clientKill("%s:%d".formatted(host, port))).next(); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java index 8fd0d4f403..d74953a6b7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java index e22b298146..1f45c373cd 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/LettuceReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java index 5008c5e68d..9bb93f3987 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,7 @@ * @author Jiahe Cai * @author Michele Mancioppi * @author John Blum + * @author Marcin Grzejszczak * @since 2.0 */ class LettuceReactiveStringCommands implements ReactiveStringCommands { @@ -105,6 +106,20 @@ public Flux> set(Publisher commands) { })); } + @Override + public Flux> setGet(Publisher commands) { + + return this.connection.execute(reactiveCommands -> Flux.from(commands).concatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getValue(), "Value must not be null"); + + return reactiveCommands.setGet(command.getKey(), command.getValue()) + .map(v -> new ByteBufferResponse<>(command, v)) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + })); + } + @Override public Flux> getSet(Publisher commands) { @@ -364,7 +379,7 @@ public Flux> bitOp(Publisher c Assert.isTrue(sourceKeys.length == 1, "BITOP NOT does not allow more than 1 source key."); yield reactiveCommands.bitopNot(destinationKey, sourceKeys[0]); } - default -> throw new IllegalArgumentException(String.format("Unknown BITOP '%s'", command.getBitOp())); + default -> throw new IllegalArgumentException("Unknown BITOP '%s'".formatted(command.getBitOp())); }; return result.map(value -> new NumericResponse<>(command, value)); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscription.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscription.java index 9098c14c84..9df426a53d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscription.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/LettuceReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java index d11f6229be..415d76bfe2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceResult.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceResult.java index 4c0a825c85..d3e2f779a1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceResult.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceScanCursor.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java index 6f5ed1a945..b3b467533c 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -59,8 +59,8 @@ protected ScanIteration doScan(CursorId cursorId, ScanOptions options) { } } - throw new IllegalArgumentException(String.format("Current scan %s state and cursor %d do not match", - state != null ? state.getCursor() : "(none)", cursorId)); + throw new IllegalArgumentException("Current scan %s state and cursor %d do not match" + .formatted(state != null ? state.getCursor() : "(none)", cursorId)); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScriptingCommands.java index de38a44747..52ec10e0ed 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScriptingCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceSentinelConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnection.java index 79bf304a91..1abd08e60e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/connection/lettuce/LettuceServerCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceServerCommands.java index 54a1dcfd6d..1b9124e138 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceServerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,7 +26,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisServerCommands; import org.springframework.data.redis.core.types.RedisClientInfo; @@ -173,7 +172,7 @@ public void killClient(String host, int port) { Assert.hasText(host, "Host for 'CLIENT KILL' must not be 'null' or 'empty'"); - String client = String.format("%s:%s", host, port); + String client = "%s:%d".formatted(host, port); connection.invoke().just(RedisServerAsyncCommands::clientKill, client); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSetCommands.java index 3e210bdedd..cb165702ff 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java index 7f15f68e8e..ad5c2281c2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/LettuceStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java index 1397fc9ce7..5dad73a71e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -36,6 +36,7 @@ * @author Mark Paluch * @author dengliming * @author John Blum + * @author Marcin Grzejszczak * @since 2.0 */ class LettuceStringCommands implements RedisStringCommands { @@ -115,6 +116,19 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op .orElse(LettuceConverters.stringToBooleanConverter(), false); } + @Override + @Nullable + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + return connection.invoke() + .just(RedisStringAsyncCommands::setGet, key, value, LettuceConverters.toSetArgs(expiration, option)); + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSubscription.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSubscription.java index 72acfef38f..32b0d97397 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSubscription.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceSubscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/lettuce/LettuceZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java index 9ebccac7b9..aeb16f6e56 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -441,8 +441,8 @@ public Set zInterWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZAggregateArgs zAggregateArgs = zAggregateArgs(aggregate, weights); @@ -456,8 +456,8 @@ public Long zInterStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(destKey, "Destination key must not be null"); Assert.notNull(sets, "Source sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZStoreArgs storeArgs = zStoreArgs(aggregate, weights); @@ -496,8 +496,8 @@ public Set zUnionWithScores(Aggregate aggregate, Weights weights, byte[]. Assert.notNull(sets, "Sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZAggregateArgs zAggregateArgs = zAggregateArgs(aggregate, weights); @@ -511,8 +511,8 @@ public Long zUnionStore(byte[] destKey, Aggregate aggregate, Weights weights, by Assert.notNull(destKey, "Destination key must not be null"); Assert.notNull(sets, "Source sets must not be null"); Assert.noNullElements(sets, "Source sets must not contain null elements"); - Assert.isTrue(weights.size() == sets.length, () -> String - .format("The number of weights (%d) must match the number of source sets (%d)", weights.size(), sets.length)); + Assert.isTrue(weights.size() == sets.length, + "The number of weights %d must match the number of source sets %d".formatted(weights.size(), sets.length)); ZStoreArgs storeArgs = zStoreArgs(aggregate, weights); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/RangeConverter.java b/src/main/java/org/springframework/data/redis/connection/lettuce/RangeConverter.java index 7777c165d9..9210d2d908 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/RangeConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/RangeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/RedisClientProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/RedisClientProvider.java index 637999d1d9..02c8ec152a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/RedisClientProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/RedisClientProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/RedisCredentialsProviderFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/RedisCredentialsProviderFactory.java index e34b8f5757..b29341b22b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/RedisCredentialsProviderFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/RedisCredentialsProviderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/lettuce/StandaloneConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/StandaloneConnectionProvider.java index 15bcd3d1bb..e812b4946b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/StandaloneConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/StandaloneConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/StaticMasterReplicaConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProvider.java index ca3d4337b0..562c2eef31 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -79,7 +79,7 @@ class StaticMasterReplicaConnectionProvider implements LettuceConnectionProvider return connectionType.cast(connection); } - throw new UnsupportedOperationException(String.format("Connection type %s not supported", connectionType)); + throw new UnsupportedOperationException("Connection type %s not supported".formatted(connectionType)); } @Override @@ -97,6 +97,6 @@ class StaticMasterReplicaConnectionProvider implements LettuceConnectionProvider }); } - throw new UnsupportedOperationException(String.format("Connection type %s not supported", connectionType)); + throw new UnsupportedOperationException("Connection type %s not supported".formatted(connectionType)); } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java index a243a44748..ae17a9ca70 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/lettuce/aot/LettuceRuntimeHints.java b/src/main/java/org/springframework/data/redis/connection/lettuce/aot/LettuceRuntimeHints.java index fc9d3ab70a..337b000195 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/aot/LettuceRuntimeHints.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/aot/LettuceRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java deleted file mode 100644 index fa662181f5..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import java.net.InetSocketAddress; -import java.util.Locale; - -import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.HighCardinalityCommandKeyNames; -import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.LowCardinalityCommandKeyNames; - -import io.lettuce.core.protocol.RedisCommand; -import io.lettuce.core.tracing.Tracing.Endpoint; -import io.micrometer.common.KeyValues; - -/** - * Default {@link LettuceObservationConvention} implementation. - * - * @author Mark Paluch - * @since 3.0 - */ -record DefaultLettuceObservationConvention( - boolean includeCommandArgsInSpanTags) implements LettuceObservationConvention { - - @Override - public KeyValues getLowCardinalityKeyValues(LettuceObservationContext context) { - - Endpoint ep = context.getRequiredEndpoint(); - KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DATABASE_SYSTEM.withValue("redis"), // - LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().name())); - - if (ep instanceof SocketAddressEndpoint endpoint) { - - if (endpoint.socketAddress()instanceof InetSocketAddress inet) { - keyValues = keyValues - .and(KeyValues.of(LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(inet.getHostString()), - LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + inet.getPort()), - LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"))); - } else { - keyValues = keyValues - .and(KeyValues.of(LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(endpoint.toString()), - LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("Unix"))); - } - } - - return keyValues; - } - - @Override - public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) { - - RedisCommand command = context.getRequiredCommand(); - - if (includeCommandArgsInSpanTags) { - - if (command.getArgs() != null) { - return KeyValues.of(HighCardinalityCommandKeyNames.STATEMENT - .withValue(command.getType().name() + " " + command.getArgs().toCommandString())); - } - } - - return KeyValues.empty(); - } - - @Override - public String getContextualName(LettuceObservationContext context) { - return context.getRequiredCommand().getType().name().toLowerCase(Locale.ROOT); - } -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java deleted file mode 100644 index e1a77d3e7b..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import io.lettuce.core.protocol.RedisCommand; -import io.lettuce.core.tracing.Tracing.Endpoint; -import io.micrometer.observation.Observation; -import io.micrometer.observation.transport.Kind; -import io.micrometer.observation.transport.SenderContext; - -import org.springframework.lang.Nullable; - -/** - * Micrometer {@link Observation.Context} holding Lettuce contextual details. - * - * @author Mark Paluch - * @since 3.0 - */ -public class LettuceObservationContext extends SenderContext { - - private volatile @Nullable RedisCommand command; - - private volatile @Nullable Endpoint endpoint; - - public LettuceObservationContext(String serviceName) { - super((carrier, key, value) -> {}, Kind.CLIENT); - setRemoteServiceName(serviceName); - } - - public RedisCommand getRequiredCommand() { - - RedisCommand local = command; - - if (local == null) { - throw new IllegalArgumentException("LettuceObservationContext is not associated with a Command"); - } - - return local; - } - - public void setCommand(RedisCommand command) { - this.command = command; - } - - public Endpoint getRequiredEndpoint() { - - Endpoint local = endpoint; - - if (local == null) { - throw new IllegalArgumentException("LettuceObservationContext is not associated with a Endpoint"); - } - - return local; - } - - public void setEndpoint(Endpoint endpoint) { - this.endpoint = endpoint; - } -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java deleted file mode 100644 index 92c1e0ee0a..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationConvention; - -/** - * {@link ObservationConvention} for {@link LettuceObservationContext}. - * - * @author Mark Paluch - * @since 3.0 - */ -interface LettuceObservationConvention extends ObservationConvention { - - @Override - default boolean supportsContext(Observation.Context context) { - return context instanceof LettuceObservationContext; - } - -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java deleted file mode 100644 index 010c5eb155..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import io.lettuce.core.protocol.CompleteableCommand; -import io.lettuce.core.protocol.RedisCommand; -import io.lettuce.core.tracing.TraceContext; -import io.lettuce.core.tracing.TraceContextProvider; -import io.lettuce.core.tracing.Tracer; -import io.lettuce.core.tracing.Tracer.Span; -import io.lettuce.core.tracing.TracerProvider; -import io.lettuce.core.tracing.Tracing; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; -import reactor.core.publisher.Mono; - -import java.net.SocketAddress; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.HighCardinalityCommandKeyNames; -import org.springframework.lang.Nullable; - -/** - * {@link Tracing} adapter using Micrometer's {@link Observation}. This adapter integrates with Micrometer to propagate - * observations into timers, distributed traces and any other registered handlers. Observations include a set of tags - * capturing Redis runtime information. - *

    Capturing full statements

    This adapter can capture full statements when enabling - * {@code includeCommandArgsInSpanTags}. You should carefully consider the impact of this setting as all command - * arguments will be captured in traces including these that may contain sensitive details. - * - * @author Mark Paluch - * @author Yanming Zhou - * @since 3.0 - */ -public class MicrometerTracingAdapter implements Tracing { - - private static final Log log = LogFactory.getLog(MicrometerTracingAdapter.class); - - private final ObservationRegistry observationRegistry; - private final String serviceName; - private final boolean includeCommandArgsInSpanTags; - - private final LettuceObservationConvention observationConvention; - - /** - * Create a new {@link MicrometerTracingAdapter} instance. - * - * @param observationRegistry must not be {@literal null}. - * @param serviceName service name to be used. - */ - public MicrometerTracingAdapter(ObservationRegistry observationRegistry, String serviceName) { - this(observationRegistry, serviceName, false); - } - - /** - * Create a new {@link MicrometerTracingAdapter} instance. - * - * @param observationRegistry must not be {@literal null}. - * @param serviceName service name to be used. - * @param includeCommandArgsInSpanTags whether to attach the full command into the trace. Use this flag with caution - * as sensitive arguments will be captured in the observation spans and metric tags. - */ - public MicrometerTracingAdapter(ObservationRegistry observationRegistry, String serviceName, - boolean includeCommandArgsInSpanTags) { - - this.observationRegistry = observationRegistry; - this.serviceName = serviceName; - this.observationConvention = new DefaultLettuceObservationConvention(includeCommandArgsInSpanTags); - this.includeCommandArgsInSpanTags = includeCommandArgsInSpanTags; - } - - @Override - public TracerProvider getTracerProvider() { - return () -> new MicrometerTracer(observationRegistry); - } - - @Override - public TraceContextProvider initialTraceContextProvider() { - return new MicrometerTraceContextProvider(observationRegistry); - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - public boolean includeCommandArgsInSpanTags() { - return includeCommandArgsInSpanTags; - } - - @Override - public Endpoint createEndpoint(SocketAddress socketAddress) { - return new SocketAddressEndpoint(socketAddress); - } - - /** - * {@link Tracer} implementation based on Micrometer's {@link ObservationRegistry}. - */ - public class MicrometerTracer extends Tracer { - - private final ObservationRegistry observationRegistry; - - public MicrometerTracer(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - } - - @Override - public Tracer.Span nextSpan() { - return this.postProcessSpan(createObservation(null)); - } - - @Override - public Tracer.Span nextSpan(TraceContext traceContext) { - return postProcessSpan(createObservation(traceContext)); - } - - private Observation createObservation(@Nullable TraceContext parentContext) { - - return RedisObservation.REDIS_COMMAND_OBSERVATION.observation(observationRegistry, () -> { - - LettuceObservationContext context = new LettuceObservationContext(serviceName); - - if (parentContext instanceof MicrometerTraceContext traceContext) { - context.setParentObservation(traceContext.observation()); - } - return context; - }); - } - - private Tracer.Span postProcessSpan(Observation observation) { - - return !observation.isNoop() ? new MicrometerSpan(observation.observationConvention(observationConvention)) - : NoOpSpan.INSTANCE; - } - } - - /** - * No-op {@link Span} implementation. - */ - static class NoOpSpan extends Tracer.Span { - - static final NoOpSpan INSTANCE = new NoOpSpan(); - - public NoOpSpan() {} - - @Override - public Tracer.Span start(RedisCommand command) { - return this; - } - - @Override - public Tracer.Span name(String name) { - return this; - } - - @Override - public Tracer.Span annotate(String value) { - return this; - } - - @Override - public Tracer.Span tag(String key, String value) { - return this; - } - - @Override - public Tracer.Span error(Throwable throwable) { - return this; - } - - @Override - public Tracer.Span remoteEndpoint(Tracing.Endpoint endpoint) { - return this; - } - - @Override - public void finish() {} - } - - /** - * Micrometer {@link Observation}-based {@link Span} implementation. - */ - static class MicrometerSpan extends Tracer.Span { - - private final Observation observation; - - private @Nullable RedisCommand command; - - public MicrometerSpan(Observation observation) { - this.observation = observation; - } - - @Override - public Span start(RedisCommand command) { - - ((LettuceObservationContext) observation.getContext()).setCommand(command); - - this.command = command; - - if (log.isDebugEnabled()) { - log.debug(String.format("Starting Observation for Command %s", command)); - } - - if (command instanceof CompleteableCommand completeableCommand) { - - completeableCommand.onComplete((o, throwable) -> { - - if (command.getOutput() != null) { - - String error = command.getOutput().getError(); - if (error != null) { - observation.highCardinalityKeyValue(HighCardinalityCommandKeyNames.ERROR.withValue(error)); - } else if (throwable != null) { - error(throwable); - } - } - - finish(); - }); - } else { - throw new IllegalArgumentException("Command " + command - + " must implement CompleteableCommand to attach Span completion to command completion"); - } - - observation.start(); - return this; - } - - @Override - public Span name(String name) { - return this; - } - - @Override - public Span annotate(String annotation) { - return this; - } - - @Override - public Span tag(String key, String value) { - observation.highCardinalityKeyValue(key, value); - return this; - } - - @Override - public Span error(Throwable throwable) { - - if (log.isDebugEnabled()) { - log.debug(String.format("Attaching error to Observation for Command %s", command)); - } - - observation.error(throwable); - return this; - } - - @Override - public Span remoteEndpoint(Endpoint endpoint) { - - ((LettuceObservationContext) observation.getContext()).setEndpoint(endpoint); - return this; - } - - @Override - public void finish() { - - if (log.isDebugEnabled()) { - log.debug(String.format("Stopping Observation for Command %s", command)); - } - - observation.stop(); - } - } - - /** - * {@link TraceContextProvider} using {@link ObservationRegistry}. - */ - record MicrometerTraceContextProvider(ObservationRegistry registry) implements TraceContextProvider { - - @Override - @Nullable - public TraceContext getTraceContext() { - - Observation observation = registry.getCurrentObservation(); - - if (observation == null) { - return null; - } - - return new MicrometerTraceContext(observation); - } - - @Override - public Mono getTraceContextLater() { - - return Mono.deferContextual(Mono::justOrEmpty).filter((it) -> { - return it.hasKey(TraceContext.class) || it.hasKey(Observation.class) - || it.hasKey(ObservationThreadLocalAccessor.KEY); - }).map((it) -> { - - if (it.hasKey(Observation.class)) { - return new MicrometerTraceContext(it.get(Observation.class)); - } - - if (it.hasKey(TraceContext.class)) { - return it.get(TraceContext.class); - } - - return new MicrometerTraceContext(it.get(ObservationThreadLocalAccessor.KEY)); - }); - } - } - - /** - * {@link TraceContext} implementation using {@link Observation}. - * - * @param observation - */ - record MicrometerTraceContext(Observation observation) implements TraceContext { - - } -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java deleted file mode 100644 index fb57b0fd82..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2013-2024 the original author 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.redis.connection.lettuce.observability; - -import io.micrometer.common.docs.KeyName; -import io.micrometer.observation.docs.ObservationDocumentation; - -/** - * A Redis-based {@link io.micrometer.observation.Observation}. - * - * @author Mark Paluch - * @since 3.0 - */ -public enum RedisObservation implements ObservationDocumentation { - - /** - * Timer created around a Redis command execution. - */ - REDIS_COMMAND_OBSERVATION { - - @Override - public String getName() { - return "spring.data.redis"; - } - - @Override - public KeyName[] getLowCardinalityKeyNames() { - return LowCardinalityCommandKeyNames.values(); - } - - @Override - public KeyName[] getHighCardinalityKeyNames() { - return HighCardinalityCommandKeyNames.values(); - } - }; - - /** - * Enums related to low cardinality key names for Redis commands. - */ - enum LowCardinalityCommandKeyNames implements KeyName { - - /** - * Database system. - */ - DATABASE_SYSTEM { - @Override - public String asString() { - return "db.system"; - } - }, - - /** - * Network transport. - */ - NET_TRANSPORT { - @Override - public String asString() { - return "net.transport"; - } - }, - - /** - * Name of the database host. - */ - NET_PEER_NAME { - @Override - public String asString() { - return "net.peer.name"; - } - }, - - /** - * Logical remote port number. - */ - NET_PEER_PORT { - @Override - public String asString() { - return "net.peer.port"; - } - }, - - /** - * Mongo peer address. - */ - NET_SOCK_PEER_ADDR { - @Override - public String asString() { - return "net.sock.peer.addr"; - } - }, - - /** - * Mongo peer port. - */ - NET_SOCK_PEER_PORT { - @Override - public String asString() { - return "net.sock.peer.port"; - } - }, - - /** - * Redis user. - */ - DB_USER { - @Override - public String asString() { - return "db.user"; - } - }, - - /** - * Redis database index. - */ - DB_INDEX { - @Override - public String asString() { - return "db.redis.database_index"; - } - }, - - /** - * Redis command value. - */ - REDIS_COMMAND { - @Override - public String asString() { - return "db.operation"; - } - } - - } - - /** - * Enums related to high cardinality key names for Redis commands. - */ - enum HighCardinalityCommandKeyNames implements KeyName { - - /** - * Redis statement. - */ - STATEMENT { - @Override - public String asString() { - return "db.statement"; - } - }, - - /** - * Redis error response. - */ - ERROR { - @Override - public String asString() { - return "spring.data.redis.command.error"; - } - } - } -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java deleted file mode 100644 index ecc6b26fe1..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; - -import io.lettuce.core.tracing.Tracing.Endpoint; - -/** - * @author Mark Paluch - */ -record SocketAddressEndpoint(SocketAddress socketAddress) implements Endpoint { - - @Override - public String toString() { - - if (socketAddress instanceof InetSocketAddress inet) { - return inet.getHostString() + ":" + inet.getPort(); - } - - return socketAddress.toString(); - } -} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java deleted file mode 100644 index e3231ef4c3..0000000000 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Integration of Micrometer Tracing for Lettuce Observability. - */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields -package org.springframework.data.redis.connection.lettuce.observability; diff --git a/src/main/java/org/springframework/data/redis/connection/stream/ByteBufferRecord.java b/src/main/java/org/springframework/data/redis/connection/stream/ByteBufferRecord.java index 990dc0ffc6..488ad046fc 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/ByteBufferRecord.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/ByteBufferRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/ByteRecord.java b/src/main/java/org/springframework/data/redis/connection/stream/ByteRecord.java index f2d97ca812..25d625ae6d 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/ByteRecord.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/ByteRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/Consumer.java b/src/main/java/org/springframework/data/redis/connection/stream/Consumer.java index 6219266652..b5c05e1e03 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/Consumer.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/Consumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -53,7 +53,7 @@ public static Consumer from(String group, String name) { @Override public String toString() { - return String.format("%s:%s", group, name); + return "%s:%s".formatted(group, name); } public String getGroup() { diff --git a/src/main/java/org/springframework/data/redis/connection/stream/MapRecord.java b/src/main/java/org/springframework/data/redis/connection/stream/MapRecord.java index 731af14240..baf61286b6 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/MapRecord.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/MapRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/ObjectRecord.java b/src/main/java/org/springframework/data/redis/connection/stream/ObjectRecord.java index 62c42775a6..7cb685dfb9 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/ObjectRecord.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/ObjectRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/PendingMessage.java b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessage.java index 7358645534..7dfb35d506 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/PendingMessage.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/stream/PendingMessages.java b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessages.java index 80a3e146db..50130ecc64 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/PendingMessages.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessages.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/stream/PendingMessagesSummary.java b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessagesSummary.java index 5716b2f34f..1addb65e8c 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/PendingMessagesSummary.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/PendingMessagesSummary.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/stream/ReadOffset.java b/src/main/java/org/springframework/data/redis/connection/stream/ReadOffset.java index 7efec2110c..0e9563d5ce 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/ReadOffset.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/ReadOffset.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/Record.java b/src/main/java/org/springframework/data/redis/connection/stream/Record.java index 4c34d5ce87..9ad9a8cd6f 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/Record.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/Record.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/RecordId.java b/src/main/java/org/springframework/data/redis/connection/stream/RecordId.java index 5791a116d4..6e79c38cc7 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/RecordId.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/RecordId.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/StreamInfo.java b/src/main/java/org/springframework/data/redis/connection/stream/StreamInfo.java index c822fcea47..4a657a03f5 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StreamInfo.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StreamInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/stream/StreamOffset.java b/src/main/java/org/springframework/data/redis/connection/stream/StreamOffset.java index 5ceb927972..df44bc663a 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StreamOffset.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StreamOffset.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/stream/StreamReadOptions.java b/src/main/java/org/springframework/data/redis/connection/stream/StreamReadOptions.java index 45c456c0bc..4729e38a41 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StreamReadOptions.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StreamReadOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -96,7 +96,7 @@ public StreamReadOptions block(Duration timeout) { */ public StreamReadOptions count(long count) { - Assert.isTrue(count > 0, "Count must be greater or equal to zero"); + Assert.isTrue(count > 0, "Count must be greater than zero"); return new StreamReadOptions(block, count, noack); } diff --git a/src/main/java/org/springframework/data/redis/connection/stream/StreamRecords.java b/src/main/java/org/springframework/data/redis/connection/stream/StreamRecords.java index bf9a34eec3..499119a1be 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StreamRecords.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StreamRecords.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -216,7 +216,7 @@ public ByteBufferRecord ofBuffer(Map value) { } else if (stream instanceof byte[]) { streamKey = ByteBuffer.wrap((byte[]) stream); } else { - throw new IllegalArgumentException(String.format("Stream key %s cannot be converted to byte buffer", stream)); + throw new IllegalArgumentException("Stream key %s cannot be converted to byte buffer".formatted(stream)); } return new ByteBufferMapBackedRecord(streamKey, id, value); diff --git a/src/main/java/org/springframework/data/redis/connection/stream/StreamSerialization.java b/src/main/java/org/springframework/data/redis/connection/stream/StreamSerialization.java index 6558727a0b..8849fc9813 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StreamSerialization.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StreamSerialization.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -59,12 +59,12 @@ static T deserialize(@Nullable RedisSerializer serializer, byte } /** - * Returns whether the given {@link RedisSerializer} is capable of serializing the {@code value} to {@literal byte[]}. + * Returns whether the given {@link RedisSerializer} is capable of serializing the {@code value} to {@code byte[]}. * * @param serializer the serializer. Can be {@literal null}. * @param value the value to serialize. * @return {@literal true} if the given {@link RedisSerializer} is capable of serializing the {@code value} to - * {@literal byte[]}. + * {@code byte[]}. */ private static boolean canSerialize(@Nullable RedisSerializer serializer, @Nullable Object value) { return serializer != null && (value == null || serializer.canSerialize(value.getClass())); diff --git a/src/main/java/org/springframework/data/redis/connection/stream/StringRecord.java b/src/main/java/org/springframework/data/redis/connection/stream/StringRecord.java index b1b8bce931..4d6c5a5bd3 100644 --- a/src/main/java/org/springframework/data/redis/connection/stream/StringRecord.java +++ b/src/main/java/org/springframework/data/redis/connection/stream/StringRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/connection/util/AbstractSubscription.java b/src/main/java/org/springframework/data/redis/connection/util/AbstractSubscription.java index d04adc7854..8ffb356306 100644 --- a/src/main/java/org/springframework/data/redis/connection/util/AbstractSubscription.java +++ b/src/main/java/org/springframework/data/redis/connection/util/AbstractSubscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/util/ByteArraySet.java b/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java index fe964dd947..2bf6e29b55 100644 --- a/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java +++ b/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/util/ByteArrayWrapper.java b/src/main/java/org/springframework/data/redis/connection/util/ByteArrayWrapper.java index 5a33c06642..883bc003c8 100644 --- a/src/main/java/org/springframework/data/redis/connection/util/ByteArrayWrapper.java +++ b/src/main/java/org/springframework/data/redis/connection/util/ByteArrayWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/connection/util/DecodeUtils.java b/src/main/java/org/springframework/data/redis/connection/util/DecodeUtils.java index 3da38d5cad..42e7bd43a6 100644 --- a/src/main/java/org/springframework/data/redis/connection/util/DecodeUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/util/DecodeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -30,8 +30,8 @@ * Simple class containing various decoding utilities. * * @author Costin Leau - * @auhtor Christoph Strobl - * @auhtor Mark Paluch + * @author Christoph Strobl + * @author Mark Paluch */ public abstract class DecodeUtils { diff --git a/src/main/java/org/springframework/data/redis/connection/zset/Aggregate.java b/src/main/java/org/springframework/data/redis/connection/zset/Aggregate.java index 083e386575..49a735eb6d 100644 --- a/src/main/java/org/springframework/data/redis/connection/zset/Aggregate.java +++ b/src/main/java/org/springframework/data/redis/connection/zset/Aggregate.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/zset/DefaultTuple.java b/src/main/java/org/springframework/data/redis/connection/zset/DefaultTuple.java index bddd817e0a..00637153eb 100644 --- a/src/main/java/org/springframework/data/redis/connection/zset/DefaultTuple.java +++ b/src/main/java/org/springframework/data/redis/connection/zset/DefaultTuple.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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. @@ -36,7 +36,7 @@ public class DefaultTuple implements Tuple { /** * Constructs a new {@link DefaultTuple}. * - * @param value {@link byte[]} of the member's raw value. + * @param value {@code byte[]} of the member's raw value. * @param score {@link Double score} of the raw value used in sorting. */ public DefaultTuple(byte[] value, Double score) { @@ -58,9 +58,8 @@ public boolean equals(@Nullable Object obj) { return true; if (obj == null) return false; - if (!(obj instanceof DefaultTuple)) + if (!(obj instanceof DefaultTuple other)) return false; - DefaultTuple other = (DefaultTuple) obj; if (score == null) { if (other.score != null) return false; diff --git a/src/main/java/org/springframework/data/redis/connection/zset/Tuple.java b/src/main/java/org/springframework/data/redis/connection/zset/Tuple.java index a706af4a09..f1aca1f897 100644 --- a/src/main/java/org/springframework/data/redis/connection/zset/Tuple.java +++ b/src/main/java/org/springframework/data/redis/connection/zset/Tuple.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/zset/Weights.java b/src/main/java/org/springframework/data/redis/connection/zset/Weights.java index a40045f274..754c48f230 100644 --- a/src/main/java/org/springframework/data/redis/connection/zset/Weights.java +++ b/src/main/java/org/springframework/data/redis/connection/zset/Weights.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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. @@ -151,11 +151,10 @@ public boolean equals(@Nullable Object o) { return true; } - if (!(o instanceof Weights)) { + if (!(o instanceof Weights that)) { return false; } - Weights that = (Weights) o; return ObjectUtils.nullSafeEquals(this.weights, that.weights); } diff --git a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java index d3f2a080d4..c730e2d74b 100644 --- a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java +++ b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import org.springframework.data.geo.GeoResults; import org.springframework.data.redis.connection.RedisConnection; @@ -51,7 +52,7 @@ abstract class AbstractOperations { // utility methods for the template internal methods abstract class ValueDeserializingRedisCallback implements RedisCallback { - private Object key; + private final Object key; public ValueDeserializingRedisCallback(Object key) { this.key = key; @@ -66,12 +67,31 @@ public final V doInRedis(RedisConnection connection) { protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection); } + private class FunctionalValueDeserializingRedisCallback extends ValueDeserializingRedisCallback { + + private final BiFunction function; + + public FunctionalValueDeserializingRedisCallback(Object key, BiFunction function) { + super(key); + this.function = function; + } + + @Nullable + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return function.apply(connection, rawKey); + } + } + final RedisTemplate template; AbstractOperations(RedisTemplate template) { this.template = template; } + ValueDeserializingRedisCallback valueCallbackFor(Object key, BiFunction function) { + return new FunctionalValueDeserializingRedisCallback(key, function); + } + RedisSerializer keySerializer() { return template.getKeySerializer(); } diff --git a/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java b/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java index 4250560c0a..d11a387f80 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/BoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java new file mode 100644 index 0000000000..8546445af4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 the original author 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.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; + +/** + * Hash Field Expiration operations bound to a certain hash key and set of hash fields. + * + * @param type of the hash field names. + * @author Mark Paluch + * @since 3.5 + */ +public interface BoundHashFieldExpirationOperations { + + /** + * Apply {@link Expiration} to the bound hash key/hash fields without any additional constraints. + * + * @param expiration the expiration definition. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + */ + default ExpireChanges expire(Expiration expiration) { + return expire(expiration, ExpirationOptions.none()); + } + + /** + * Apply {@link Expiration} to the bound hash key/hash fields given {@link ExpirationOptions expiration options}. + * + * @param expiration the expiration definition. + * @param options expiration options. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + */ + ExpireChanges expire(Expiration expiration, ExpirationOptions options); + + /** + * Set time to live for the bound hash key/hash fields. + * + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expire(Duration timeout); + + /** + * Set the expiration for the bound hash key/hash fields as a {@literal date} timestamp. + * + * @param expireAt must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expireAt(Instant expireAt); + + /** + * Remove the expiration from the bound hash key/hash fields. + * + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges persist(); + + /** + * Get the time to live for bound hash key/hash fields in seconds. + * + * @return the actual expirations in seconds for the hash fields. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(); + + /** + * Get the time to live for the bound hash key/hash fields and convert it to the given {@link TimeUnit}. + * + * @param timeUnit must not be {@literal null}. + * @return the actual expirations for the hash fields in the given time unit. {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(TimeUnit timeUnit); + +} diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index b7c30dc1b7..0355588ea4 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -29,6 +30,7 @@ * @author Christoph Strobl * @author Ninad Divadkar * @author Mark Paluch + * @author Tihomir Mateev */ public interface BoundHashOperations extends BoundKeyOperations { @@ -61,7 +63,7 @@ public interface BoundHashOperations extends BoundKeyOperations { /** * Get values for given {@code keys} from the hash at the bound key. Values are in the order of the requested keys - * Absent field values are represented using {@code null} in the resulting {@link List}. + * Absent field values are represented using {@literal null} in the resulting {@link List}. * * @param keys must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. @@ -90,7 +92,7 @@ public interface BoundHashOperations extends BoundKeyOperations { Double increment(HK key, double delta); /** - * Return a random key (aka field) from the hash stored at the bound key. + * Return a random key from the hash stored at the bound key. * * @return {@literal null} if the hash does not exist or when used in pipeline / transaction. * @since 2.6 @@ -110,10 +112,10 @@ public interface BoundHashOperations extends BoundKeyOperations { Map.Entry randomEntry(); /** - * Return a random keys (aka fields) from the hash stored at the bound key. If the provided {@code count} argument is - * positive, return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same key multiple times. In this case, the - * number of returned keys is the absolute value of the specified count. + * Return a random keys from the hash stored at the bound key. If the provided {@code count} argument is positive, + * return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is negative, the + * behavior changes and the command is allowed to return the same key multiple times. In this case, the number of + * returned keys is the absolute value of the specified count. * * @param count number of keys to return. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -213,8 +215,45 @@ public interface BoundHashOperations extends BoundKeyOperations { */ Cursor> scan(ScanOptions options); + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration + * operation. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations hashExpiration() { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), this::keys); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations hashExpiration(HK... hashFields) { + return hashExpiration(Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations hashExpiration(Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), () -> hashFields); + } + /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/BoundKeyExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/BoundKeyExpirationOperations.java new file mode 100644 index 0000000000..b50431b209 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/BoundKeyExpirationOperations.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 the original author 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.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; + +/** + * Key Expiration operations bound to a key. + * + * @author Mark Paluch + * @since 3.5 + */ +public interface BoundKeyExpirationOperations { + + /** + * Apply {@link Expiration} to the bound key without any additional constraints. + * + * @param expiration the expiration definition. + * @return changes to the key. {@literal null} when used in pipeline / transaction. + */ + default ExpireChanges.ExpiryChangeState expire(Expiration expiration) { + return expire(expiration, ExpirationOptions.none()); + } + + /** + * Apply {@link Expiration} to the bound key given {@link ExpirationOptions expiration options}. + * + * @param expiration the expiration definition. + * @param options expiration options. + * @return changes to the key. {@literal null} when used in pipeline / transaction. + */ + @Nullable + ExpireChanges.ExpiryChangeState expire(Expiration expiration, ExpirationOptions options); + + /** + * Set time to live for the bound key. + * + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @return changes to the key. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: EXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges.ExpiryChangeState expire(Duration timeout); + + /** + * Set the expiration for the bound key as a {@literal date} timestamp. + * + * @param expireAt must not be {@literal null}. + * @return changes to the key. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: EXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges.ExpiryChangeState expireAt(Instant expireAt); + + /** + * Remove the expiration from the bound key. + * + * @return changes to the key. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: PERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges.ExpiryChangeState persist(); + + /** + * Get the time to live for the bound key in seconds. + * + * @return the actual expirations in seconds for the key. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: TTL + * @since 3.5 + */ + @Nullable + Expirations.TimeToLive getTimeToLive(); + + /** + * Get the time to live for the bound key and convert it to the given {@link TimeUnit}. + * + * @param timeUnit must not be {@literal null}. + * @return the actual expirations for the key in the given time unit. {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: TTL + * @since 3.5 + */ + @Nullable + Expirations.TimeToLive getTimeToLive(TimeUnit timeUnit); + +} diff --git a/src/main/java/org/springframework/data/redis/core/BoundKeyOperations.java b/src/main/java/org/springframework/data/redis/core/BoundKeyOperations.java index d719b05fb2..bb8b438a75 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundKeyOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundKeyOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -51,6 +51,16 @@ public interface BoundKeyOperations { @Nullable DataType getType(); + /** + * Returns a bound operations object to perform expiration operations on the bound key. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundKeyExpirationOperations expiration() { + return new DefaultBoundKeyExpirationOperations<>(getOperations(), getKey()); + } + /** * Returns the expiration of this key. * @@ -127,4 +137,10 @@ default Boolean expireAt(Instant expireAt) { * @param newKey new key. Must not be {@literal null}. */ void rename(K newKey); + + /** + * @return never {@literal null}. + */ + RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/BoundListOperations.java b/src/main/java/org/springframework/data/redis/core/BoundListOperations.java index 9c429debac..481ee8e674 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundListOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -213,7 +213,25 @@ public interface BoundListOperations extends BoundKeyOperations { Long remove(long count, Object value); /** - * Get element at {@code index} form list at the bound key. + * Returns the first element from the list at the bound {@code key}. + * + * @return {@literal null} when used in pipeline / transaction. + * @since 3.4 + */ + @Nullable + V getFirst(); + + /** + * Returns the last element from the list at the bound {@code key}. + * + * @return {@literal null} when used in pipeline / transaction. + * @since 3.4 + */ + @Nullable + V getLast(); + + /** + * Get element at {@code index} from list at the bound key. * * @param index * @return {@literal null} when used in pipeline / transaction. @@ -231,6 +249,7 @@ public interface BoundListOperations extends BoundKeyOperations { * @since 2.4 * @see Redis Documentation: LPOS */ + @Nullable Long indexOf(V value); /** @@ -242,6 +261,7 @@ public interface BoundListOperations extends BoundKeyOperations { * @since 2.4 * @see Redis Documentation: LPOS */ + @Nullable Long lastIndexOf(V value); /** diff --git a/src/main/java/org/springframework/data/redis/core/BoundOperationsProxyFactory.java b/src/main/java/org/springframework/data/redis/core/BoundOperationsProxyFactory.java index a65b1e200c..2c4bfc5f49 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundOperationsProxyFactory.java +++ b/src/main/java/org/springframework/data/redis/core/BoundOperationsProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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. @@ -70,7 +70,7 @@ public T createProxy(Class boundOperationsInterface, Object key, DataType proxyFactory.addAdvice( new BoundOperationsMethodInterceptor(key, operations, boundOperationsInterface, operationsTarget, delegate)); - return (T) proxyFactory.getProxy(); + return (T) proxyFactory.getProxy(getClass().getClassLoader()); } Method lookupRequiredMethod(Method method, Class targetClass, boolean considerKeyArgument) { @@ -143,10 +143,10 @@ public Object invoke(MethodInvocation invocation) throws Throwable { delegate.rename(invocation.getArguments()[0]); yield null; } - case "getOperations" -> delegate.getOps(); - default -> method.getDeclaringClass() == boundOperationsInterface - ? doInvoke(invocation, method, operationsTarget, true) - : doInvoke(invocation, method, delegate, false); + case "getOperations" -> delegate.getOperations(); + default -> + method.getDeclaringClass() == boundOperationsInterface ? doInvoke(invocation, method, operationsTarget, true) + : doInvoke(invocation, method, delegate, false); }; } @@ -234,12 +234,15 @@ public void rename(Object newKey) { key = newKey; } + @Override public DataType getType() { return type; } - public RedisOperations getOps() { + @Override + public RedisOperations getOperations() { return ops; } + } } diff --git a/src/main/java/org/springframework/data/redis/core/BoundSetOperations.java b/src/main/java/org/springframework/data/redis/core/BoundSetOperations.java index b9d03ab655..424b210b67 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -185,6 +185,7 @@ public interface BoundSetOperations extends BoundKeyOperations { * @deprecated since 3.0, use {@link #difference(Object)} instead to follow a consistent method naming scheme. */ @Nullable + @Deprecated(since = "3.0") default Set diff(K key) { return difference(key); } @@ -209,6 +210,7 @@ default Set diff(K key) { * @deprecated since 3.0, use {@link #difference(Collection)} instead to follow a consistent method naming scheme. */ @Nullable + @Deprecated(since = "3.0") default Set diff(Collection keys) { return difference(keys); } diff --git a/src/main/java/org/springframework/data/redis/core/BoundStreamOperations.java b/src/main/java/org/springframework/data/redis/core/BoundStreamOperations.java index b589c99163..d3add2dad7 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundStreamOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundStreamOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -20,6 +20,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.stream.Consumer; import org.springframework.data.redis.connection.stream.MapRecord; import org.springframework.data.redis.connection.stream.ReadOffset; @@ -58,6 +59,18 @@ public interface BoundStreamOperations { @Nullable RecordId add(Map body); + /** + * Append a record to the stream {@code key} with the specified options. + * + * @param content record content as Map. + * @param xAddOptions additional parameters for the {@literal XADD} call. + * @return the record Id. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: XADD + * @since 3.4 + */ + @Nullable + RecordId add(Map content, XAddOptions xAddOptions); + /** * Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the * number of IDs passed in case certain IDs do not exist. diff --git a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java index b11d7c6657..429b053ff4 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -28,6 +28,7 @@ * @author Mark Paluch * @author Jiahe Cai * @author Christoph Strobl + * @author Marcin Grzejszczak */ public interface BoundValueOperations extends BoundKeyOperations { @@ -49,6 +50,33 @@ public interface BoundValueOperations extends BoundKeyOperations { */ void set(V value, long timeout, TimeUnit unit); + /** + * Set the {@code value} and expiration {@code timeout} for the bound key. Return the old string stored at key, or + * {@literal null} if key did not exist. An error is returned and SET aborted if the value stored at key is not a + * string. + * + * @param value must not be {@literal null}. + * @param timeout + * @param unit must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: SET + * @since 3.5 + */ + V setGet(V value, long timeout, TimeUnit unit); + + /** + * Set the {@code value} and expiration {@code timeout} for the bound key. Return the old string stored at key, or + * {@literal null} if key did not exist. An error is returned and SET aborted if the value stored at key is not a + * string. + * + * @param value must not be {@literal null}. + * @param duration expiration duration + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: SET + * @since 3.5 + */ + V setGet(V value, Duration duration); + /** * Set the {@code value} and expiration {@code timeout} for the bound key. * diff --git a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java index 724cb5cd11..160d0836ad 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/BulkMapper.java b/src/main/java/org/springframework/data/redis/core/BulkMapper.java index 686b285f66..77fa487672 100644 --- a/src/main/java/org/springframework/data/redis/core/BulkMapper.java +++ b/src/main/java/org/springframework/data/redis/core/BulkMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/CloseSuppressingInvocationHandler.java b/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java index cdd8c26f1f..1f85f9d389 100644 --- a/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java +++ b/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/ClusterOperations.java b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java index 833ca61620..f96c8c0d39 100644 --- a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/ClusterOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/ClusterOperationsEditor.java index 1f5fe55704..a662e7e437 100644 --- a/src/main/java/org/springframework/data/redis/core/ClusterOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/ClusterOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/ConvertingCursor.java b/src/main/java/org/springframework/data/redis/core/ConvertingCursor.java index e80ea71d1a..b0d5e1e5fd 100644 --- a/src/main/java/org/springframework/data/redis/core/ConvertingCursor.java +++ b/src/main/java/org/springframework/data/redis/core/ConvertingCursor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/Cursor.java b/src/main/java/org/springframework/data/redis/core/Cursor.java index fa60b0e489..e52cfc5d77 100644 --- a/src/main/java/org/springframework/data/redis/core/Cursor.java +++ b/src/main/java/org/springframework/data/redis/core/Cursor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/DefaultBoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java new file mode 100644 index 0000000000..fdb45c57bc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author 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.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link BoundHashFieldExpirationOperations}. + * + * @author Mark Paluch + * @since 3.5 + */ +class DefaultBoundHashFieldExpirationOperations implements BoundHashFieldExpirationOperations { + + private final HashOperations operations; + private final H key; + private final Supplier> hashFields; + + public DefaultBoundHashFieldExpirationOperations(HashOperations operations, H key, + Supplier> hashFields) { + + this.operations = operations; + this.key = key; + this.hashFields = hashFields; + } + + @Override + public ExpireChanges expire(Expiration expiration, ExpirationOptions options) { + return operations.expire(key, expiration, options, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expire(Duration timeout) { + return operations.expire(key, timeout, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expireAt(Instant expireAt) { + return operations.expireAt(key, expireAt, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges persist() { + return operations.persist(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive() { + return operations.getTimeToLive(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive(TimeUnit timeUnit) { + return operations.getTimeToLive(key, timeUnit, getHashKeys()); + } + + private Collection getHashKeys() { + + Collection hks = hashFields.get(); + + Assert.state(hks != null, "Hash keys must not be null"); + return hks; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundKeyExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundKeyExpirationOperations.java new file mode 100644 index 0000000000..9ff186cd3e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundKeyExpirationOperations.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025 the original author 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.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; + +/** + * Default {@link BoundKeyExpirationOperations} implementation. + * + * @author Mark Paluch + * @since 3.5 + */ +class DefaultBoundKeyExpirationOperations implements BoundKeyExpirationOperations { + + private final RedisOperations operations; + private final K key; + + public DefaultBoundKeyExpirationOperations(RedisOperations operations, K key) { + this.operations = operations; + this.key = key; + } + + @Nullable + @Override + public ExpireChanges.ExpiryChangeState expire(Expiration expiration, ExpirationOptions options) { + return operations.expire(key, expiration, options); + } + + @Nullable + @Override + public ExpireChanges.ExpiryChangeState expire(Duration timeout) { + + Boolean expire = operations.expire(key, timeout); + + return toExpiryChangeState(expire); + } + + @Nullable + @Override + public ExpireChanges.ExpiryChangeState expireAt(Instant expireAt) { + return toExpiryChangeState(operations.expireAt(key, expireAt)); + } + + @Nullable + @Override + public ExpireChanges.ExpiryChangeState persist() { + return toExpiryChangeState(operations.persist(key)); + } + + @Nullable + @Override + public Expirations.TimeToLive getTimeToLive() { + + Long expire = operations.getExpire(key); + + return expire == null ? null : Expirations.TimeToLive.of(expire, TimeUnit.SECONDS); + } + + @Nullable + @Override + public Expirations.TimeToLive getTimeToLive(TimeUnit timeUnit) { + + Long expire = operations.getExpire(key, timeUnit); + + return expire == null ? null : Expirations.TimeToLive.of(expire, timeUnit); + + } + + @Nullable + private static ExpireChanges.ExpiryChangeState toExpiryChangeState(@Nullable Boolean result) { + + if (result == null) { + return null; + } + + return ExpireChanges.ExpiryChangeState.of(result); + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java index 77a285ae0f..7215430082 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/DefaultGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java index e451cf97d7..9456bff358 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index 1bfb9c3467..ccdfbee704 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,6 +15,8 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -22,9 +24,14 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,6 +41,7 @@ * @author Costin Leau * @author Christoph Strobl * @author Ninad Divadkar + * @author Tihomir Mateev */ class DefaultHashOperations extends AbstractOperations implements HashOperations { @@ -210,6 +218,86 @@ public Boolean putIfAbsent(K key, HK hashKey, HV value) { return execute(connection -> connection.hSetNX(rawKey, rawHashKey, rawHashValue)); } + @Override + public ExpireChanges expire(K key, Duration duration, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + + byte[] rawKey = rawKey(key); + byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); + boolean hasMillis = TimeoutUtils.hasMillis(duration); + + List raw = execute(connection -> TimeoutUtils.hasMillis(duration) + ? connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys) + : connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys)); + + return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; + } + + @Override + public ExpireChanges expireAt(K key, Instant instant, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + + byte[] rawKey = rawKey(key); + byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); + long millis = instant.toEpochMilli(); + + List raw = execute(connection -> TimeoutUtils.containsSplitSecond(millis) + ? connection.hashCommands().hpExpireAt(rawKey, millis, rawHashKeys) + : connection.hashCommands().hExpireAt(rawKey, instant.getEpochSecond(), rawHashKeys)); + + return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; + } + + @Override + public ExpireChanges expire(K key, Expiration expiration, ExpirationOptions options, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + byte[] rawKey = rawKey(key); + byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); + List raw = execute( + connection -> connection.hashCommands().applyHashFieldExpiration(rawKey, expiration, options, rawHashKeys)); + + return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; + } + + @Override + public ExpireChanges persist(K key, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + byte[] rawKey = rawKey(key); + byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); + + List raw = execute(connection -> connection.hashCommands().hPersist(rawKey, rawHashKeys)); + + return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; + } + + @Override + public Expirations getTimeToLive(K key, TimeUnit timeUnit, Collection hashKeys) { + + if(timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { + throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); + } + + List orderedKeys = List.copyOf(hashKeys); + + byte[] rawKey = rawKey(key); + byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); + + List raw = execute( + connection -> TimeUnit.MILLISECONDS.equals(timeUnit) ? connection.hashCommands().hpTtl(rawKey, rawHashKeys) + : connection.hashCommands().hTtl(rawKey, timeUnit, rawHashKeys)); + + if (raw == null) { + return null; + } + + Timeouts timeouts = new Timeouts(TimeUnit.MILLISECONDS.equals(timeUnit) ? timeUnit : TimeUnit.SECONDS, raw); + return Expirations.of(timeUnit, orderedKeys, timeouts); + } + @Override public List values(K key) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHyperLogLogOperations.java index 41d8dc0406..30f0c4c3e9 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHyperLogLogOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHyperLogLogOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/DefaultListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java index 61855b8b0d..ce0268869c 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/DefaultReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java index 12192babd2..6e92f6a749 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index fec1f2e7e1..282f229913 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,16 +19,25 @@ import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.connection.ReactiveHashCommands.HashExpireCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -63,8 +72,7 @@ public Mono remove(H key, Object... hashKeys) { Assert.noNullElements(hashKeys, "Hash keys must not contain null elements"); return createMono(hashCommands -> Flux.fromArray(hashKeys) // - .map(hashKey -> (HK) hashKey) - .map(this::rawHashKey) // + .map(hashKey -> (HK) hashKey).map(this::rawHashKey) // .collectList() // .flatMap(hks -> hashCommands.hDel(rawKey(key), hks))); } @@ -86,8 +94,8 @@ public Mono get(H key, Object hashKey) { Assert.notNull(key, "Key must not be null"); Assert.notNull(hashKey, "Hash key must not be null"); - return createMono(hashCommands -> hashCommands.hGet(rawKey(key), rawHashKey((HK) hashKey)) - .map(this::readHashValue)); + return createMono( + hashCommands -> hashCommands.hGet(rawKey(key), rawHashKey((HK) hashKey)).map(this::readHashValue)); } @Override @@ -109,8 +117,8 @@ public Mono increment(H key, HK hashKey, long delta) { Assert.notNull(key, "Key must not be null"); Assert.notNull(hashKey, "Hash key must not be null"); - return template.doCreateMono(connection -> connection.numberCommands() - .hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); + return template + .doCreateMono(connection -> connection.numberCommands().hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); } @Override @@ -119,8 +127,8 @@ public Mono increment(H key, HK hashKey, double delta) { Assert.notNull(key, "Key must not be null"); Assert.notNull(hashKey, "Hash key must not be null"); - return template.doCreateMono(connection -> connection.numberCommands() - .hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); + return template + .doCreateMono(connection -> connection.numberCommands().hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); } @Override @@ -137,8 +145,7 @@ public Mono> randomEntry(H key) { Assert.notNull(key, "Key must not be null"); - return createMono(hashCommands -> hashCommands.hRandFieldWithValues(rawKey(key))) - .map(this::deserializeHashEntry); + return createMono(hashCommands -> hashCommands.hRandFieldWithValues(rawKey(key))).map(this::deserializeHashEntry); } @Override @@ -235,6 +242,82 @@ public Flux> scan(H key, ScanOptions options) { .map(this::deserializeHashEntry)); } + @Override + public Mono> expire(H key, Duration timeout, Collection hashKeys) { + return expire(key, Expiration.from(timeout), ExpirationOptions.none(), hashKeys); + } + + @Override + public Mono> expire(H key, Expiration expiration, ExpirationOptions options, + Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + ByteBuffer rawKey = rawKey(key); + List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); + + Mono> raw = createFlux(connection -> { + return connection + .applyHashFieldExpiration( + Mono.just(HashExpireCommand.expire(rawHashKeys, expiration).from(rawKey).withOptions(options))) + .map(NumericResponse::getOutput); + }).collectList(); + + return raw.map(values -> ExpireChanges.of(orderedKeys, values)); + } + + @Nullable + @Override + public Mono> expireAt(H key, Instant expireAt, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + ByteBuffer rawKey = rawKey(key); + List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); + + Mono> raw = createFlux(connection -> connection.hExpireAt(rawKey, expireAt, rawHashKeys)).collectList(); + + return raw.map(values -> ExpireChanges.of(orderedKeys, values)); + } + + @Nullable + @Override + public Mono> persist(H key, Collection hashKeys) { + + List orderedKeys = List.copyOf(hashKeys); + ByteBuffer rawKey = rawKey(key); + List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); + + Mono> raw = createFlux(connection -> connection.hPersist(rawKey, rawHashKeys)).collectList(); + + return raw.map(values -> ExpireChanges.of(orderedKeys, values)); + } + + @Nullable + @Override + public Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys) { + + if (timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { + throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); + } + + List orderedKeys = List.copyOf(hashKeys); + ByteBuffer rawKey = rawKey(key); + List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); + + Mono> raw = createFlux(connection -> { + + if (TimeUnit.MILLISECONDS.equals(timeUnit)) { + return connection.hpTtl(rawKey, rawHashKeys); + } + return connection.hTtl(rawKey, rawHashKeys); + }).collectList(); + + return raw.map(values -> { + + Timeouts timeouts = new Timeouts(TimeUnit.MILLISECONDS.equals(timeUnit) ? timeUnit : TimeUnit.SECONDS, values); + return Expirations.of(timeUnit, orderedKeys, timeouts); + }); + } + @Override public Mono delete(H key) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java index d9c3516741..c3acc12718 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java index 74297cd674..1a26f750a7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java index 13b546689e..4294bca879 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveStreamOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveStreamOperations.java index 432cf77283..f7365adba5 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveStreamOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveStreamOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -33,6 +33,7 @@ import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.ReactiveStreamCommands; import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.stream.ByteBufferRecord; import org.springframework.data.redis.connection.stream.Consumer; @@ -60,6 +61,7 @@ * @author Christoph Strobl * @author Marcin Zielinski * @author John Blum + * @author jinkshower * @since 2.2 */ class DefaultReactiveStreamOperations implements ReactiveStreamOperations { @@ -146,6 +148,18 @@ public Mono add(Record record) { return createMono(streamCommands -> streamCommands.xAdd(serializeRecord(input))); } + @Override + public Mono add(Record record, XAddOptions xAddOptions) { + + Assert.notNull(record.getStream(), "Key must not be null"); + Assert.notNull(record.getValue(), "Body must not be null"); + Assert.notNull(xAddOptions, "XAddOptions must not be null"); + + MapRecord input = StreamObjectMapper.toMapRecord(this, record); + + return createMono(streamCommands -> streamCommands.xAdd(serializeRecord(input), xAddOptions)); + } + @Override public Flux> claim(K key, String consumerGroup, String newOwner, XClaimOptions xClaimOptions) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java index 0f0ac35200..8cf8d54985 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -77,6 +77,18 @@ public Mono set(K key, V value, Duration timeout) { stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)); } + @Override + public Mono setGet(K key, V value, Duration timeout) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(timeout, "Duration must not be null"); + + return createMono(stringCommands -> + stringCommands.setGet(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)) + .map(this::readRequiredValue); + } + @Override public Mono setIfAbsent(K key, V value) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java index 40869055e1..65ae76a139 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -27,6 +27,7 @@ import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; @@ -38,7 +39,6 @@ import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.util.ByteUtils; -import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -745,8 +745,13 @@ private V readValue(ByteBuffer buffer) { private V readRequiredValue(ByteBuffer buffer) { - return RedisAssertions.requireNonNull(readValue(buffer), - () -> new InvalidDataAccessApiUsageException("Deserialized sorted set value is null")); + V value = readValue(buffer); + + if (value == null) { + throw new InvalidDataAccessApiUsageException("Deserialized sorted set value is null"); + } + + return value; } private TypedTuple readTypedTuple(Tuple raw) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultSetOperations.java index 5949af8d73..9c8b87dad3 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/DefaultStreamOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultStreamOperations.java index 52eaa318ae..3cb27d1dcd 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultStreamOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultStreamOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -26,6 +26,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions; import org.springframework.data.redis.connection.stream.ByteRecord; import org.springframework.data.redis.connection.stream.Consumer; @@ -54,6 +55,7 @@ * @author Christoph Strobl * @author Marcin Zielinski * @author John Blum + * @author jinkshower * @since 2.2 */ class DefaultStreamOperations extends AbstractOperations implements StreamOperations { @@ -136,6 +138,21 @@ public RecordId add(Record record) { return execute(connection -> connection.xAdd(binaryRecord)); } + @Nullable + @Override + @SuppressWarnings("unchecked") + public RecordId add(Record record, XAddOptions options) { + + Assert.notNull(record, "Record must not be null"); + Assert.notNull(options, "XAddOptions must not be null"); + + MapRecord input = StreamObjectMapper.toMapRecord(this, record); + + ByteRecord binaryRecord = input.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer()); + + return execute(connection -> connection.streamCommands().xAdd(binaryRecord, options)); + } + @Override public List> claim(K key, String consumerGroup, String newOwner, XClaimOptions xClaimOptions) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultTypedTuple.java b/src/main/java/org/springframework/data/redis/core/DefaultTypedTuple.java index c26cdbe912..3ae83af6c5 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultTypedTuple.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultTypedTuple.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 9938d5a0ec..357dda961f 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -23,8 +23,8 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.BitFieldSubCommands; +import org.springframework.data.redis.connection.DefaultedRedisConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; @@ -37,6 +37,7 @@ * @author Jennifer Hickey * @author Christoph Strobl * @author Jiahe Cai + * @author Ehsan Alemzadeh */ class DefaultValueOperations extends AbstractOperations implements ValueOperations { @@ -46,79 +47,39 @@ class DefaultValueOperations extends AbstractOperations implements V @Override public V get(Object key) { - - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.get(rawKey); - } - }); + return execute(valueCallbackFor(key, DefaultedRedisConnection::get)); } @Nullable @Override public V getAndDelete(K key) { - - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.getDel(rawKey); - } - }); + return execute(valueCallbackFor(key, DefaultedRedisConnection::getDel)); } @Nullable @Override public V getAndExpire(K key, long timeout, TimeUnit unit) { - - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.getEx(rawKey, Expiration.from(timeout, unit)); - } - }); + return execute( + valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.from(timeout, unit)))); } @Nullable @Override public V getAndExpire(K key, Duration timeout) { - - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.getEx(rawKey, Expiration.from(timeout)); - } - }); + return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.from(timeout)))); } @Nullable @Override public V getAndPersist(K key) { - - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.getEx(rawKey, Expiration.persistent()); - } - }); + return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.persistent()))); } @Override public V getAndSet(K key, V newValue) { byte[] rawValue = rawValue(newValue); - return execute(new ValueDeserializingRedisCallback(key) { - - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - return connection.getSet(rawKey, rawValue); - } - }); + return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getSet(rawKey, rawValue))); } @Override @@ -233,15 +194,10 @@ public Boolean multiSetIfAbsent(Map m) { @Override public void set(K key, V value) { + byte[] rawKey = rawKey(key); byte[] rawValue = rawValue(value); - execute(new ValueDeserializingRedisCallback(key) { - @Override - protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { - connection.set(rawKey, rawValue); - return null; - } - }); + execute(connection -> connection.set(rawKey, rawValue)); } @Override @@ -250,34 +206,28 @@ public void set(K key, V value, long timeout, TimeUnit unit) { byte[] rawKey = rawKey(key); byte[] rawValue = rawValue(value); - execute(new RedisCallback() { - - @Override - public Object doInRedis(RedisConnection connection) throws DataAccessException { + execute(connection -> connection.set(rawKey, rawValue, Expiration.from(timeout, unit), SetOption.upsert())); + } - potentiallyUsePsetEx(connection); - return null; - } + @Override + public V setGet(K key, V value, long timeout, TimeUnit unit) { + return doSetGet(key, value, Expiration.from(timeout, unit)); + } - public void potentiallyUsePsetEx(RedisConnection connection) { + @Override + public V setGet(K key, V value, Duration duration) { + return doSetGet(key, value, Expiration.from(duration)); + } - if (!TimeUnit.MILLISECONDS.equals(unit) || !failsafeInvokePsetEx(connection)) { - connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue); - } - } + private V doSetGet(K key, V value, Expiration duration) { - private boolean failsafeInvokePsetEx(RedisConnection connection) { + byte[] rawValue = rawValue(value); + return execute(new ValueDeserializingRedisCallback(key) { - boolean failed = false; - try { - connection.pSetEx(rawKey, timeout, rawValue); - } catch (UnsupportedOperationException ignore) { - // in case the connection does not support pSetEx return false to allow fallback to other operation. - failed = true; - } - return !failed; + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.stringCommands().setGet(rawKey, rawValue, duration, SetOption.UPSERT); } - }); } @@ -286,7 +236,7 @@ public Boolean setIfAbsent(K key, V value) { byte[] rawKey = rawKey(key); byte[] rawValue = rawValue(value); - return execute(connection -> connection.setNX(rawKey, rawValue)); + return execute(connection -> connection.set(rawKey, rawValue, Expiration.persistent(), SetOption.ifAbsent())); } @Override diff --git a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java index 059f746707..030974bf8c 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/ExpireChanges.java b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java new file mode 100644 index 0000000000..10bef1e2d2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java @@ -0,0 +1,203 @@ +/* + * Copyright 2025 the original author 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.redis.core; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Value Object linking a number of keys to their {@link ExpiryChangeState} retaining the order of the original source. + * Dedicated higher level methods interpret raw values retrieved from a Redis Client. + *
      + *
    1. {@link #ok()} returns keys for which the time to live has been set
    2. + *
    3. {@link #expired()} returns keys that have been expired
    4. + *
    5. {@link #missed()} returns keys for which the time to live could not be set because they do not exist
    6. + *
    7. {@link #skipped()} returns keys for which the time to live has not been set because a precondition was not + * met
    8. + *
    + * + * @author Christoph Strobl + * @since 3.5 + */ +public class ExpireChanges { + + private final Map changes; + + ExpireChanges(Map changes) { + this.changes = changes; + } + + /** + * Factory Method to create {@link ExpireChanges} from raw sources. + * + * @param fields the fields to associated with the raw values in states. Defines the actual order of entries within + * {@link ExpireChanges}. + * @param states the raw Redis state change values. + * @return new instance of {@link ExpireChanges}. + * @param the key type used + */ + public static ExpireChanges of(List fields, List states) { + + Assert.isTrue(fields.size() == states.size(), "Keys and States must have the same number of elements"); + + if (fields.size() == 1) { + return new ExpireChanges<>(Map.of(fields.iterator().next(), stateFromValue(states.iterator().next()))); + } + + Map target = CollectionUtils.newLinkedHashMap(fields.size()); + for (int i = 0; i < fields.size(); i++) { + target.put(fields.get(i), stateFromValue(states.get(i))); + } + return new ExpireChanges<>(Collections.unmodifiableMap(target)); + } + + /** + * @return an ordered {@link List} of the status changes. + */ + public List stateChanges() { + return List.copyOf(changes.values()); + } + + /** + * @return the status change for the given {@literal key}, or {@literal null} if {@link ExpiryChangeState} does not + * contain an entry for it. + */ + public ExpiryChangeState stateOf(K key) { + return changes.get(key); + } + + /** + * @return {@literal true} if all changes are {@link ExpiryChangeState#OK}. + */ + public boolean allOk() { + return allMach(ExpiryChangeState.OK::equals); + } + + /** + * @return {@literal true} if all changes are either ok {@link ExpiryChangeState#OK} or + * {@link ExpiryChangeState#EXPIRED}. + */ + public boolean allChanged() { + return allMach(it -> ExpiryChangeState.OK.equals(it) || ExpiryChangeState.EXPIRED.equals(it)); + } + + /** + * @return an ordered list of if all changes are {@link ExpiryChangeState#OK}. + */ + public Set ok() { + return filterByState(ExpiryChangeState.OK); + } + + /** + * @return an ordered list of if all changes are {@link ExpiryChangeState#EXPIRED}. + */ + public Set expired() { + return filterByState(ExpiryChangeState.EXPIRED); + } + + /** + * @return an ordered list of if all changes are {@link ExpiryChangeState#DOES_NOT_EXIST}. + */ + public Set missed() { + return filterByState(ExpiryChangeState.DOES_NOT_EXIST); + } + + /** + * @return an ordered list of if all changes are {@link ExpiryChangeState#CONDITION_NOT_MET}. + */ + public Set skipped() { + return filterByState(ExpiryChangeState.CONDITION_NOT_MET); + } + + public boolean allMach(Predicate predicate) { + return changes.values().stream().allMatch(predicate); + } + + private Set filterByState(ExpiryChangeState filter) { + return changes.entrySet().stream().filter(entry -> entry.getValue().equals(filter)).map(Map.Entry::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static ExpiryChangeState stateFromValue(Number value) { + return ExpiryChangeState.of(value); + } + + public record ExpiryChangeState(long value) { + + public static final ExpiryChangeState DOES_NOT_EXIST = new ExpiryChangeState(-2L); + public static final ExpiryChangeState CONDITION_NOT_MET = new ExpiryChangeState(0L); + public static final ExpiryChangeState OK = new ExpiryChangeState(1L); + public static final ExpiryChangeState EXPIRED = new ExpiryChangeState(2L); + + static ExpiryChangeState of(boolean value) { + return value ? OK : CONDITION_NOT_MET; + } + + static ExpiryChangeState of(Number value) { + return switch (value.intValue()) { + case -2 -> DOES_NOT_EXIST; + case 0 -> CONDITION_NOT_MET; + case 1 -> OK; + case 2 -> EXPIRED; + default -> new ExpiryChangeState(value.longValue()); + }; + } + + public boolean isOk() { + return OK.equals(this); + } + + public boolean isExpired() { + return EXPIRED.equals(this); + } + + public boolean isMissing() { + return DOES_NOT_EXIST.equals(this); + } + + public boolean isSkipped() { + return CONDITION_NOT_MET.equals(this); + } + + @Override + public boolean equals(Object o) { + + if (o == this) { + return true; + } + + if (!(o instanceof ExpiryChangeState that)) { + return false; + } + + return this.value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/GeoOperations.java b/src/main/java/org/springframework/data/redis/core/GeoOperations.java index f06f2a88fc..6b20c07789 100644 --- a/src/main/java/org/springframework/data/redis/core/GeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/GeoOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/GeoOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/GeoOperationsEditor.java index c55b5f55ab..5de01d1f56 100644 --- a/src/main/java/org/springframework/data/redis/core/GeoOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/GeoOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/HashMapperProvider.java b/src/main/java/org/springframework/data/redis/core/HashMapperProvider.java index 2b1b77547d..ae0df547b7 100644 --- a/src/main/java/org/springframework/data/redis/core/HashMapperProvider.java +++ b/src/main/java/org/springframework/data/redis/core/HashMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index 8e98e49f97..1dc34db4c3 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,11 +15,18 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; import org.springframework.lang.Nullable; /** @@ -28,6 +35,7 @@ * @author Costin Leau * @author Christoph Strobl * @author Ninad Divadkar + * @author Tihomir Mateev */ public interface HashOperations { @@ -61,7 +69,7 @@ public interface HashOperations { /** * Get values for given {@code hashKeys} from hash at {@code key}. Values are in the order of the requested keys - * Absent field values are represented using {@code null} in the resulting {@link List}. + * Absent field values are represented using {@literal null} in the resulting {@link List}. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -90,7 +98,7 @@ public interface HashOperations { Double increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -112,10 +120,10 @@ public interface HashOperations { Map.Entry randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -221,8 +229,131 @@ public interface HashOperations { */ Cursor> scan(H key, ScanOptions options); + /** + * Set time to live for given {@code hashKey} . + * + * @param key must not be {@literal null}. + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expire(H key, Duration timeout, Collection hashKeys); + + /** + * Set the expiration for given {@code hashKeys} as a {@literal date} timestamp. + * + * @param key must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expireAt(H key, Instant expireAt, Collection hashKeys); + + /** + * Apply the expiration for given {@code hashKeys}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @see Redis Documentation: HPEXPIRE + * @see Redis Documentation: HEXPIREAT + * @see Redis Documentation: HPEXPIREAT + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges expire(H key, Expiration expiration, ExpirationOptions options, Collection hashKeys); + + /** + * Remove the expiration from given {@code hashKeys} . + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return changes to the hash fields. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges persist(H key, Collection hashKeys); + + /** + * Get the time to live for {@code hashKeys} in seconds. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return the actual expirations in seconds for the hash fields. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + default Expirations getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); + } + + /** + * Get the time to live for {@code hashKeys} and convert it to the given {@link TimeUnit}. + * + * @param key must not be {@literal null}. + * @param timeUnit must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return the actual expirations for the hash fields. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration operation. + * + * @param key must not be {@literal null}. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> keys(key)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, HK... hashFields) { + return expiration(key, Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> hashFields); + } + /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/HashOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/HashOperationsEditor.java index 30d31d81f2..8f8a1a751f 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/HyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/HyperLogLogOperations.java index 0c16fa1be4..9adb6252f4 100644 --- a/src/main/java/org/springframework/data/redis/core/HyperLogLogOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HyperLogLogOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/HyperLogLogOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/HyperLogLogOperationsEditor.java index abf47663bc..4087bf24d4 100644 --- a/src/main/java/org/springframework/data/redis/core/HyperLogLogOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/HyperLogLogOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/IndexWriter.java b/src/main/java/org/springframework/data/redis/core/IndexWriter.java index a7f1cf5f60..73e2a27347 100644 --- a/src/main/java/org/springframework/data/redis/core/IndexWriter.java +++ b/src/main/java/org/springframework/data/redis/core/IndexWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,23 +33,26 @@ /** * {@link IndexWriter} takes care of writing secondary index structures to - * Redis. Depending on the type of {@link IndexedData} it uses eg. Sets with specific names to add actually referenced - * keys to. While doing so {@link IndexWriter} also keeps track of all indexes associated with the root types key, which + * Redis. Depending on the type of {@link IndexedData}, it uses Sets with specific names to add actually referenced keys + * to. While doing so {@link IndexWriter} also keeps track of all indexes associated with the root types key, which * allows to remove the root key from all indexes in case of deletion. * * @author Christoph Strobl * @author Rob Winch + * @author Mark Paluch * @since 1.7 */ class IndexWriter { + private static final byte[] SEPARATOR = ":".getBytes(); + private static final byte[] IDX = "idx".getBytes(); + private final RedisConnection connection; private final RedisConverter converter; /** * Creates new {@link IndexWriter}. * - * @param keyspace The key space to write index values to. Must not be {@literal null}. * @param connection must not be {@literal null}. * @param converter must not be {@literal null}. */ @@ -127,7 +130,7 @@ public void removeKeyFromIndexes(String keyspace, Object key) { Assert.notNull(key, "Key must not be null"); byte[] binKey = toBytes(key); - byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx")); + byte[] indexHelperKey = createIndexKey(keyspace, binKey); for (byte[] indexKey : connection.sMembers(indexHelperKey)) { @@ -147,10 +150,10 @@ public void removeKeyFromIndexes(String keyspace, Object key) { */ public void removeAllIndexes(String keyspace) { - Set potentialIndex = connection.keys(toBytes(keyspace + ":*")); + Set potentialIndex = connection.keys(createIndexKey(keyspace, "*")); if (!potentialIndex.isEmpty()) { - connection.del(potentialIndex.toArray(new byte[potentialIndex.size()][])); + connection.del(potentialIndex.toArray(new byte[0][])); } } @@ -162,7 +165,7 @@ private void removeKeyFromExistingIndexes(byte[] key, Iterable inde } /** - * Remove given key from all indexes matching {@link IndexedData#getIndexName()}: + * Remove given key from all indexes matching {@link IndexedData#getIndexName()}. * * @param key * @param indexedData @@ -171,8 +174,7 @@ protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) Assert.notNull(indexedData, "IndexedData must not be null"); - Set existingKeys = connection - .keys(toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":*")); + Set existingKeys = connection.keys(createIndexKey(indexedData.getKeyPrefix(), "*")); if (!CollectionUtils.isEmpty(existingKeys)) { for (byte[] existingKey : existingKeys) { @@ -216,12 +218,12 @@ protected void addKeyToIndex(byte[] key, IndexedData indexedData) { return; } - byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName() + ":"); + byte[] indexKey = toBytes(indexedData.getKeyPrefix(), SEPARATOR); indexKey = ByteUtils.concat(indexKey, toBytes(value)); connection.sAdd(indexKey, key); // keep track of indexes used for the object - connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey); + connection.sAdd(createIndexKey(indexedData.getKeyspace(), key), indexKey); } else if (indexedData instanceof GeoIndexedPropertyValue propertyValue) { Object value = propertyValue.getValue(); @@ -229,15 +231,51 @@ protected void addKeyToIndex(byte[] key, IndexedData indexedData) { return; } - byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName()); + byte[] indexKey = toBytes(indexedData.getKeyPrefix()); connection.geoAdd(indexKey, propertyValue.getPoint(), key); // keep track of indexes used for the object - connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey); + connection.sAdd(createIndexKey(indexedData.getKeyspace(), key), indexKey); } else { throw new IllegalArgumentException( - String.format("Cannot write index data for unknown index type %s", indexedData.getClass())); + "Cannot write index data for unknown index type %s".formatted(indexedData.getClass())); + } + } + + private byte[] createIndexKey(String keyspace, byte[] key) { + return createIndexKey(keyspace, key, IDX); + } + + private byte[] createIndexKey(Object... items) { + + Object[] elements = new Object[items.length + (items.length - 1)]; + + int j = 0; + for (int i = 0; i < items.length; i++) { + + elements[j++] = items[i]; + if (items.length - 1 > i) { + elements[j++] = SEPARATOR; + } } + + return toBytes(elements); + } + + private byte[] toBytes(Object... values) { + + byte[][] arrays = new byte[values.length][]; + + for (int i = 0; i < values.length; i++) { + + if (values[i] instanceof byte[] bb) { + arrays[i] = bb; + } else { + arrays[i] = toBytes(values[i]); + } + } + + return ByteUtils.concatAll(arrays); } private byte[] toBytes(@Nullable Object source) { @@ -254,10 +292,9 @@ private byte[] toBytes(@Nullable Object source) { return converter.getConversionService().convert(source, byte[].class); } - throw new InvalidDataAccessApiUsageException(String.format( - "Cannot convert %s to binary representation for index key generation; " - + "Are you missing a Converter; Did you register a non PathBasedRedisIndexDefinition that might apply to a complex type", - source.getClass())); + throw new InvalidDataAccessApiUsageException(("Cannot convert %s to binary representation for index key generation;" + + " Are you missing a Converter; Did you register a non PathBasedRedisIndexDefinition" + + " that might apply to a complex type").formatted(source.getClass())); } /** diff --git a/src/main/java/org/springframework/data/redis/core/KeyBoundCursor.java b/src/main/java/org/springframework/data/redis/core/KeyBoundCursor.java index 2cc66d5d79..a0c553509a 100644 --- a/src/main/java/org/springframework/data/redis/core/KeyBoundCursor.java +++ b/src/main/java/org/springframework/data/redis/core/KeyBoundCursor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/KeyScanOptions.java b/src/main/java/org/springframework/data/redis/core/KeyScanOptions.java index 0cbb843732..888157c525 100644 --- a/src/main/java/org/springframework/data/redis/core/KeyScanOptions.java +++ b/src/main/java/org/springframework/data/redis/core/KeyScanOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/ListOperations.java b/src/main/java/org/springframework/data/redis/core/ListOperations.java index f5c8948385..8d08c61aa1 100644 --- a/src/main/java/org/springframework/data/redis/core/ListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ListOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -34,6 +34,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author dengliming + * @author Lee Jaeheon */ public interface ListOperations { @@ -368,7 +369,31 @@ default V move(K sourceKey, Direction from, K destinationKey, Direction to, Dura Long remove(K key, long count, Object value); /** - * Get element at {@code index} form list at {@code key}. + * Returns the first element from the list at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 3.4 + */ + @Nullable + default V getFirst(K key) { + return index(key, 0); + } + + /** + * Returns the last element from the list at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 3.4 + */ + @Nullable + default V getLast(K key) { + return index(key, -1); + } + + /** + * Get element at {@code index} from list at {@code key}. * * @param key must not be {@literal null}. * @param index @@ -525,7 +550,8 @@ default V rightPop(K key, Duration timeout) { V rightPopAndLeftPush(K sourceKey, K destinationKey); /** - * Remove the last element from list at {@code sourceKey}, append it to {@code destinationKey} and return its value.
    + * Remove the last element from list at {@code sourceKey}, append it to {@code destinationKey} and return its + * value.
    * Blocks connection until element available or {@code timeout} reached. * * @param sourceKey must not be {@literal null}. @@ -539,7 +565,8 @@ default V rightPop(K key, Duration timeout) { V rightPopAndLeftPush(K sourceKey, K destinationKey, long timeout, TimeUnit unit); /** - * Remove the last element from list at {@code sourceKey}, append it to {@code destinationKey} and return its value.
    + * Remove the last element from list at {@code sourceKey}, append it to {@code destinationKey} and return its + * value.
    * Blocks connection until element available or {@code timeout} reached. * * @param sourceKey must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java index cf8fc18daa..4c7834d885 100644 --- a/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/PartialUpdate.java b/src/main/java/org/springframework/data/redis/core/PartialUpdate.java index f516b1f65f..400333efdc 100644 --- a/src/main/java/org/springframework/data/redis/core/PartialUpdate.java +++ b/src/main/java/org/springframework/data/redis/core/PartialUpdate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/ReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java index 76471777ae..b159d532f6 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index 0f2950654a..4c8c0986d8 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,17 +19,25 @@ import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; /** * Reactive Redis operations for Hash Commands. *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl @@ -66,7 +74,7 @@ public interface ReactiveHashOperations { /** * Get values for given {@code hashKeys} from hash at {@code key}. Values are in the order of the requested keys. - * Absent field values are represented using {@code null} in the resulting {@link List}. + * Absent field values are represented using {@literal null} in the resulting {@link List}. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -95,7 +103,7 @@ public interface ReactiveHashOperations { Mono increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return @@ -115,10 +123,10 @@ public interface ReactiveHashOperations { Mono> randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -230,10 +238,91 @@ default Flux> scan(H key) { */ Flux> scan(H key, ScanOptions options); + /** + * Set time to live for given {@literal hashKeys} stored within {@literal key}. + * + * @param key must not be {@literal null}. + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting changes to the hash fields. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + Mono> expire(H key, Duration timeout, Collection hashKeys); + + /** + * Set time to live for given {@literal hashKeys} stored within {@literal key}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options additional options to apply. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting changes to the hash fields. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + Mono> expire(H key, Expiration expiration, ExpirationOptions options, Collection hashKeys); + + /** + * Set the expiration for given {@code hashKey} as a {@literal date} timestamp. + * + * @param key must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting changes to the hash fields. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + Mono> expireAt(H key, Instant expireAt, Collection hashKeys); + + /** + * Remove the expiration from given {@code hashKey} . + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting changes to the hash fields. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + Mono> persist(H key, Collection hashKeys); + + /** + * Get the time to live for {@code hashKey} in seconds. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting {@link Expirations} of the hash fields. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + default Mono> getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); + } + + /** + * Get the time to live for {@code hashKey} and convert it to the given {@link TimeUnit}. + * + * @param key must not be {@literal null}. + * @param timeUnit must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a {@link Mono} emitting {@link Expirations} of the hash fields. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); + /** * Removes the given {@literal key}. * * @param key must not be {@literal null}. */ Mono delete(H key); + } diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java index e0ed1f6148..671a99b493 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/ReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java index 6a701c2eac..a3a69bbd0f 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +26,7 @@ import org.springframework.data.redis.core.ListOperations.MoveFrom; import org.springframework.data.redis.core.ListOperations.MoveTo; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -33,8 +34,8 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl @@ -277,7 +278,31 @@ default Mono move(MoveFrom from, MoveTo to, Duration timeout) { Mono remove(K key, long count, Object value); /** - * Get element at {@code index} form list at {@code key}. + * Returns the first element from the list at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 3.4 + */ + @Nullable + default Mono getFirst(K key) { + return index(key, 0); + } + + /** + * Returns the last element from the list at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 3.4 + */ + @Nullable + default Mono getLast(K key) { + return index(key, -1); + } + + /** + * Get element at {@code index} from list at {@code key}. * * @param key must not be {@literal null}. * @param index @@ -336,8 +361,8 @@ default Mono move(MoveFrom from, MoveTo to, Duration timeout) { * * @param key must not be {@literal null}. * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either - * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be - * used to wait indefinitely. Durations between zero and one second are not supported. + * {@link Duration#ZERO} or greater {@literal 1 second}, must not be {@literal null}. A timeout of zero can + * be used to wait indefinitely. Durations between zero and one second are not supported. * @return * @see Redis Documentation: BLPOP */ @@ -369,8 +394,8 @@ default Mono move(MoveFrom from, MoveTo to, Duration timeout) { * * @param key must not be {@literal null}. * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either - * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be - * used to wait indefinitely. Durations between zero and one second are not supported. + * {@link Duration#ZERO} or greater {@literal 1 second}, must not be {@literal null}. A timeout of zero can + * be used to wait indefinitely. Durations between zero and one second are not supported. * @return * @see Redis Documentation: BRPOP */ @@ -393,8 +418,8 @@ default Mono move(MoveFrom from, MoveTo to, Duration timeout) { * @param sourceKey must not be {@literal null}. * @param destinationKey must not be {@literal null}. * @param timeout maximal duration to wait until an entry in the list at {@code sourceKey} is available. Must be - * either {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero - * can be used to wait indefinitely. Durations between zero and one second are not supported. + * either {@link Duration#ZERO} or greater {@literal 1 second}, must not be {@literal null}. A timeout of + * zero can be used to wait indefinitely. Durations between zero and one second are not supported. * @return * @see Redis Documentation: BRPOPLPUSH */ diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java index 3a22e9a771..fe5be55d0a 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/ReactiveRedisOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java index c02d1eea81..686277f0df 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,13 +22,17 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; + import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.ReactiveSubscription.Message; import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.hash.HashMapper; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.PatternTopic; @@ -45,11 +49,12 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl + * @author Dahye Anne Lee * @since 2.0 */ public interface ReactiveRedisOperations { @@ -239,6 +244,16 @@ default Mono>> listenToPatternLater(String... */ Mono hasKey(K key); + /** + * Get the number of given {@code keys} that exists. + * + * @param keys must not be {@literal null} or {@literal empty}. + * @return the number of existing keys in redis. 0 if there are no existing keys. + * @see Redis Documentation: EXISTS + * @since 3.5 + */ + Mono countExistingKeys(Collection keys); + /** * Determine the type stored at {@code key}. * @@ -373,6 +388,22 @@ default Flux scan() { */ Mono expireAt(K key, Instant expireAt); + /** + * Set the expiration for given {@code key}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options must not be {@literal null}. + * @throws IllegalArgumentException any of required arguments is {@literal null}. + * @see Redis Documentation: EXPIRE + * @see Redis Documentation: PEXPIRE + * @see Redis Documentation: EXPIREAT + * @see Redis Documentation: PEXPIREAT + * @see Redis Documentation: PERSIST + * @since 3.5 + */ + Mono expire(K key, Expiration expiration, ExpirationOptions options); + /** * Remove the expiration from given {@code key}. * @@ -429,6 +460,20 @@ default Flux execute(RedisScript script, List keys) { return execute(script, keys, Collections.emptyList()); } + /** + * Executes the given {@link RedisScript} + * + * @param script The script to execute. Must not be {@literal null}. + * @param keys keys that need to be passed to the script. Must not be {@literal null}. + * @param args args that need to be passed to the script. Must not be {@literal null}. + * @return result value of the script {@link Flux#empty()} if {@link RedisScript#getResultType()} is {@literal null}, + * likely indicating a throw-away status reply (i.e. "OK"). + * @since 3.4 + */ + default Flux execute(RedisScript script, List keys, Object... args) { + return execute(script, keys, Arrays.asList(args)); + } + /** * Executes the given {@link RedisScript} * diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisSessionCallback.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisSessionCallback.java index 3511b7ed02..8db54083fe 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisSessionCallback.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisSessionCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/core/ReactiveRedisTemplate.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java index 7816a744a8..3741aa751d 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,13 +22,17 @@ import java.nio.ByteBuffer; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.connection.ReactiveKeyCommands; import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; @@ -36,6 +40,7 @@ import org.springframework.data.redis.core.script.DefaultReactiveScriptExecutor; import org.springframework.data.redis.core.script.ReactiveScriptExecutor; import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.hash.HashMapper; import org.springframework.data.redis.hash.ObjectHashMapper; import org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer; @@ -58,13 +63,14 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl * @author Petromir Dzhunev * @author John Blum + * @author Dahye Anne Lee * @param the Redis key type against which the template works (usually a String) * @param the Redis value type against which the template works * @since 2.0 @@ -322,6 +328,14 @@ public Mono hasKey(K key) { return doCreateMono(connection -> connection.keyCommands().exists(rawKey(key))); } + @Override + public Mono countExistingKeys(Collection keys) { + + Assert.notNull(keys, "Keys must not be null"); + + return doCreateMono(connection -> connection.keyCommands().exists(rawKeys(keys))); + } + @Override public Mono type(K key) { @@ -454,6 +468,19 @@ public Mono expireAt(K key, Instant expireAt) { return doCreateMono(connection -> connection.keyCommands().pExpireAt(rawKey(key), expireAt)); } + @Override + public Mono expire(K key, Expiration expiration, ExpirationOptions options) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(expiration, "Expiration at must not be null"); + Assert.notNull(options, "ExpirationOptions at must not be null"); + + Mono just = Mono + .just(ReactiveKeyCommands.ExpireCommand.expire(rawKey(key), expiration).withOptions(options)); + return doCreateMono(connection -> connection.keyCommands().applyExpiration(just)) + .map(ReactiveRedisConnection.BooleanResponse::getOutput).map(ExpireChanges.ExpiryChangeState::of); + } + @Override public Mono persist(K key) { @@ -672,6 +699,17 @@ private ByteBuffer rawKey(K key) { return getSerializationContext().getKeySerializationPair().getWriter().write(key); } + private List rawKeys(Collection keys) { + + List rawKeys = new ArrayList<>(keys.size()); + + for (K key : keys) { + rawKeys.add(rawKey(key)); + } + + return rawKeys; + } + @Nullable private K readKey(ByteBuffer buffer) { return getSerializationContext().getKeySerializationPair().getReader().read(buffer); diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java index eb9e55e431..c3cbad9445 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -27,8 +27,8 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveStreamOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveStreamOperations.java index fc3ca29651..341eafe6b7 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveStreamOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveStreamOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -26,6 +26,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.stream.ByteBufferRecord; import org.springframework.data.redis.connection.stream.Consumer; import org.springframework.data.redis.connection.stream.MapRecord; @@ -54,6 +55,7 @@ * @author Dengliming * @author Marcin Zielinski * @author John Blum + * @author jinkshower * @since 2.2 */ public interface ReactiveStreamOperations extends HashMapperProvider { @@ -94,6 +96,48 @@ default Mono acknowledge(String group, Record record) { return acknowledge(record.getRequiredStream(), group, record.getId()); } + /** + * Append a record to the stream {@code key} with the specified options. + * + * @param key the stream key. + * @param content record content as Map. + * @param xAddOptions parameters for the {@literal XADD} call. + * @return the {@link Mono} emitting the {@link RecordId}. + * @see Redis Documentation: XADD + * @since 3.4 + */ + default Mono add(K key, Map content, XAddOptions xAddOptions) { + return add(StreamRecords.newRecord().in(key).ofMap(content), xAddOptions); + } + + /** + * Append a record, backed by a {@link Map} holding the field/value pairs, to the stream with the specified options. + * + * @param record the record to append. + * @param xAddOptions parameters for the {@literal XADD} call. + * @return the {@link Mono} emitting the {@link RecordId}. + * @see Redis Documentation: XADD + * @since 3.4 + */ + @SuppressWarnings("unchecked") + default Mono add(MapRecord record, XAddOptions xAddOptions) { + return add((Record) record, xAddOptions); + } + + /** + * Append the record, backed by the given value, to the stream with the specified options. + * The value will be hashed and serialized. + * + * @param record must not be {@literal null}. + * @param xAddOptions parameters for the {@literal XADD} call. Must not be {@literal null}. + * @return the {@link Mono} emitting the {@link RecordId}. + * @see MapRecord + * @see ObjectRecord + * @see Redis Documentation: XADD + * @since 3.4 + */ + Mono add(Record record, XAddOptions xAddOptions); + /** * Append one or more records to the stream {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveStringRedisTemplate.java b/src/main/java/org/springframework/data/redis/core/ReactiveStringRedisTemplate.java index a5a915e6e7..a593214d7f 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveStringRedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveStringRedisTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/ReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java index 2a0f44404c..9eff052005 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -30,8 +30,8 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Jiahe Cai @@ -54,16 +54,28 @@ public interface ReactiveValueOperations { * @param key must not be {@literal null}. * @param value * @param timeout must not be {@literal null}. - * @see Redis Documentation: SETEX + * @see Redis Documentation: SET */ Mono set(K key, V value, Duration timeout); + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old string stored at key, or empty + * if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @see Redis Documentation: SETEX + * @since 3.5 + */ + Mono setGet(K key, V value, Duration timeout); + /** * Set {@code key} to hold the string {@code value} if {@code key} is absent. * * @param key must not be {@literal null}. * @param value - * @see Redis Documentation: SETNX + * @see Redis Documentation: SET */ Mono setIfAbsent(K key, V value); @@ -162,7 +174,7 @@ public interface ReactiveValueOperations { /** * Get multiple {@code keys}. Values are in the order of the requested keys. Absent field values are represented using - * {@code null} in the resulting {@link List}. + * {@literal null} in the resulting {@link List}. * * @param keys must not be {@literal null}. * @see Redis Documentation: MGET @@ -281,6 +293,7 @@ public interface ReactiveValueOperations { * @param command must not be {@literal null}. * @return * @since 2.1 + * @see Redis Documentation: BITFIELD */ Mono> bitField(K key, BitFieldSubCommands command); @@ -288,6 +301,7 @@ public interface ReactiveValueOperations { * Removes the given {@literal key}. * * @param key must not be {@literal null}. + * @see Redis Documentation: DEL */ Mono delete(K key); } diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java index 552f008c24..e98bfd5929 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,8 +37,8 @@ *

    * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl @@ -507,8 +507,8 @@ default Flux> scan(K key) { * * @param key must not be {@literal null}. * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either - * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be - * used to wait indefinitely. Durations between zero and one second are not supported. + * {@link Duration#ZERO} or greater {@literal 1 second}, must not be {@literal null}. A timeout of zero can + * be used to wait indefinitely. Durations between zero and one second are not supported. * @return * @see Redis Documentation: ZPOPMIN * @since 2.6 @@ -541,8 +541,8 @@ default Flux> scan(K key) { * * @param key must not be {@literal null}. * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either - * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be - * used to wait indefinitely. Durations between zero and one second are not supported. + * {@link Duration#ZERO} or greater {@literal 1 second}, must not be {@literal null}. A timeout of zero can + * be used to wait indefinitely. Durations between zero and one second are not supported. * @return * @see Redis Documentation: ZPOPMIN * @since 2.6 diff --git a/src/main/java/org/springframework/data/redis/core/RedisAccessor.java b/src/main/java/org/springframework/data/redis/core/RedisAccessor.java index 09c8decc24..5753347654 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisAccessor.java +++ b/src/main/java/org/springframework/data/redis/core/RedisAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/RedisCallback.java b/src/main/java/org/springframework/data/redis/core/RedisCallback.java index 0c40d0702b..9ad8b41306 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCallback.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -32,13 +32,13 @@ public interface RedisCallback { /** * Method called by {@link RedisTemplate} with an active {@link RedisConnection}. *

    - * Callback code need not care about activating/opening or closing the {@link RedisConnection}, - * nor handling {@link Exception exceptions}. + * Callback code need not care about activating/opening or closing the {@link RedisConnection}, nor handling + * {@link Exception exceptions}. * * @param connection active {@link RedisConnection Redis connection}. - * @return the {@link Object result} of the operation performed in the callback or {@code null}. + * @return the {@link Object result} of the operation performed in the callback or {@literal null}. * @throws DataAccessException if the operation performed by the callback fails to execute in the context of Redis - * using the given {@link RedisConnection}. + * using the given {@link RedisConnection}. */ @Nullable T doInRedis(RedisConnection connection) throws DataAccessException; diff --git a/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java b/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java index b9ac36c83d..f13ea55dd9 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java +++ b/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index 5621a216d9..1454fd33c2 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -77,8 +77,8 @@ public enum RedisCommand { EVALSHA("rw", 2), // EXEC("rw", 0, 0), // EXISTS("r", 1, 1), // - EXPIRE("rw", 2, 2), // - EXPIREAT("rw", 2, 2), // + EXPIRE("rw", 2), // + EXPIREAT("rw", 2), // // -- F FLUSHALL("w", 0, 0), // FLUSHDB("w", 0, 0), // @@ -107,6 +107,13 @@ public enum RedisCommand { HSET("w", 3, 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // + HEXPIRE("w", 5), // + HEXPIREAT("w", 5), // + HPEXPIRE("w", 5), // + HPEXPIREAT("w", 5), // + HPERSIST("w", 4), // + HTTL("r", 4), // + HPTTL("r", 4), // // -- I INCR("rw", 1), // INCRBYFLOAT("rw", 2, 2), // @@ -135,8 +142,8 @@ public enum RedisCommand { MULTI("rw", 0, 0), // // -- P PERSIST("rw", 1, 1), // - PEXPIRE("rw", 2, 2), // - PEXPIREAT("rw", 2, 2), // + PEXPIRE("rw", 2), // + PEXPIREAT("rw", 2), // PING("r", 0, 0), // PSETEX("w", 3), // PSUBSCRIBE("r", 1), // @@ -377,16 +384,17 @@ public void validateArgumentCount(int argumentCount) { if (requiresArguments()) { if (requiresExactNumberOfArguments()) { if (argumentCount != this.maxArgs) { - throw newIllegalArgumentException("%s command requires %d %s", name(), this.maxArgs, arguments(this.maxArgs)); + throw new IllegalArgumentException( + "%s command requires %d %s".formatted(name(), this.maxArgs, arguments(this.maxArgs))); } } if (argumentCount < this.minArgs) { - throw newIllegalArgumentException("%s command requires at least %d %s", name(), this.minArgs, - arguments(this.maxArgs)); + throw new IllegalArgumentException( + "%s command requires at least %d %s".formatted(name(), this.minArgs, arguments(this.maxArgs))); } if (this.maxArgs > 0 && argumentCount > this.maxArgs) { - throw newIllegalArgumentException("%s command requires at most %s %s", name(), this.maxArgs, - arguments(this.maxArgs)); + throw new IllegalArgumentException( + "%s command requires at most %s %s".formatted(name(), this.maxArgs, arguments(this.maxArgs))); } } } @@ -394,9 +402,4 @@ public void validateArgumentCount(int argumentCount) { private String arguments(int count) { return count == 1 ? "argument" : "arguments"; } - - private IllegalArgumentException newIllegalArgumentException(String message, Object... arguments) { - return new IllegalArgumentException(String.format(message, arguments)); - } - } diff --git a/src/main/java/org/springframework/data/redis/core/RedisConnectionUtils.java b/src/main/java/org/springframework/data/redis/core/RedisConnectionUtils.java index 20e4954ae0..3e8193a15e 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisConnectionUtils.java +++ b/src/main/java/org/springframework/data/redis/core/RedisConnectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -495,14 +495,14 @@ public Object intercept(Object obj, Method method, Object[] args) throws Throwab if (isPotentiallyThreadBoundCommand(commandToExecute)) { if (log.isDebugEnabled()) { - log.debug(String.format("Invoke '%s' on bound connection", method.getName())); + log.debug("Invoke '%s' on bound connection".formatted(method.getName())); } return invoke(method, obj, args); } if (log.isDebugEnabled()) { - log.debug(String.format("Invoke '%s' on unbound connection", method.getName())); + log.debug("Invoke '%s' on unbound connection".formatted(method.getName())); } RedisConnection connection = factory.getConnection(); @@ -587,7 +587,7 @@ public RedisConnection getRequiredConnection() { * Override the existing {@link RedisConnection} handle with the given {@link RedisConnection}. Reset the handle if * given {@literal null}. *

    - * Used for releasing the Connection on suspend (with a {@code null} argument) and setting a fresh Connection on + * Used for releasing the Connection on suspend (with a {@literal null} argument) and setting a fresh Connection on * resume. */ protected void setConnection(@Nullable RedisConnection connection) { @@ -646,7 +646,7 @@ public interface RedisConnectionProxy extends RedisConnection, RawTargetAccess { *

    * This will typically be the native driver {@link RedisConnection} or a wrapper from a connection pool. * - * @return the underlying {@link RedisConnection} (never {@link null}). + * @return the underlying {@link RedisConnection} (never {@literal null}). */ RedisConnection getTargetConnection(); diff --git a/src/main/java/org/springframework/data/redis/core/RedisHash.java b/src/main/java/org/springframework/data/redis/core/RedisHash.java index b0cb2a1ea1..4ffcb57d2e 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisHash.java +++ b/src/main/java/org/springframework/data/redis/core/RedisHash.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisKeyExpiredEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java index a57f5fee93..3ebed4eb48 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index 6aaabbe156..4cff5b30f3 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; +import org.springframework.context.SmartLifecycle; import org.springframework.core.convert.ConversionService; import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueAdapter; @@ -102,13 +106,16 @@ * @since 1.7 */ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter - implements InitializingBean, ApplicationContextAware, ApplicationListener { + implements InitializingBean, SmartLifecycle, ApplicationContextAware, ApplicationListener { /** * Time To Live in seconds that phantom keys should live longer than the actual key. */ private static final int PHANTOM_KEY_TTL = 300; + private final Log logger = LogFactory.getLog(getClass()); + private final AtomicReference state = new AtomicReference<>(State.CREATED); + private RedisOperations redisOps; private RedisConverter converter; private @Nullable RedisMessageListenerContainer messageListenerContainer; @@ -120,6 +127,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter private @Nullable String keyspaceNotificationsConfigParameter = null; private ShadowCopy shadowCopy = ShadowCopy.DEFAULT; + /** + * Lifecycle state of this factory. + */ + enum State { + CREATED, STARTING, STARTED, STOPPING, STOPPED, DESTROYED; + } + /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default * {@link RedisCustomConversions}. @@ -201,7 +215,7 @@ public Object put(Object id, Object item, String keyspace) { && this.expirationListener.get() == null) { if (rdo.getTimeToLive() != null && rdo.getTimeToLive() > 0) { - initKeyExpirationListener(); + initKeyExpirationListener(this.messageListenerContainer); } } @@ -332,12 +346,12 @@ public T delete(Object id, String keyspace, Class type) { } @Override - public List getAllOf(String keyspace) { + public List getAllOf(String keyspace) { return getAllOf(keyspace, Object.class, -1, -1); } @Override - public Iterable getAllOf(String keyspace, Class type) { + public List getAllOf(String keyspace, Class type) { return getAllOf(keyspace, type, -1, -1); } @@ -557,11 +571,11 @@ public void clear() { } /** - * Creates a new {@link byte[] key} using the given {@link String keyspace} and {@link String id}. + * Creates a new {@code byte[] key} using the given {@link String keyspace} and {@link String id}. * * @param keyspace {@link String name} of the Redis {@literal keyspace}. * @param id {@link String} identifying the key. - * @return a {@link byte[]} constructed from the {@link String keyspace} and {@link String id}. + * @return a {@code byte[]} constructed from the {@link String keyspace} and {@link String id}. */ public byte[] createKey(String keyspace, String id) { return toBytes(keyspace + ":" + id); @@ -571,8 +585,7 @@ public byte[] createKey(String keyspace, String id) { * Convert given source to binary representation using the underlying {@link ConversionService}. */ public byte[] toBytes(Object source) { - return source instanceof byte[] bytes ? bytes - : getConverter().getConversionService().convert(source, byte[].class); + return source instanceof byte[] bytes ? bytes : getConverter().getConversionService().convert(source, byte[].class); } private String toString(Object value) { @@ -592,7 +605,7 @@ private T readBackTimeToLiveIfSet(@Nullable byte[] key, @Nullable T target) RedisPersistentEntity entity = this.converter.getMappingContext().getRequiredPersistentEntity(target.getClass()); - if (entity.hasExplictTimeToLiveProperty()) { + if (entity.hasExplicitTimeToLiveProperty()) { RedisPersistentProperty ttlProperty = entity.getExplicitTimeToLiveProperty(); @@ -686,6 +699,11 @@ public void setShadowCopy(ShadowCopy shadowCopy) { this.shadowCopy = shadowCopy; } + @Override + public boolean isRunning() { + return State.STARTED.equals(this.state.get()); + } + /** * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() * @since 1.8 @@ -696,22 +714,61 @@ public void afterPropertiesSet() { if (this.managedListenerContainer) { initMessageListenerContainer(); } + } + + @Override + public void start() { + + State current = this.state.getAndUpdate(state -> isCreatedOrStopped(state) ? State.STARTING : state); + + if (isCreatedOrStopped(current)) { - if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) { - initKeyExpirationListener(); + messageListenerContainer.start(); + + if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) { + initKeyExpirationListener(this.messageListenerContainer); + } + + this.state.set(State.STARTED); } } - public void destroy() throws Exception { + private static boolean isCreatedOrStopped(@Nullable State state) { + return State.CREATED.equals(state) || State.STOPPED.equals(state); + } + + @Override + public void stop() { + + if (state.compareAndSet(State.STARTED, State.STOPPING)) { + + KeyExpirationEventMessageListener listener = this.expirationListener.get(); + if (listener != null) { + + if (this.expirationListener.compareAndSet(listener, null)) { + try { + listener.destroy(); + } catch (Exception e) { + logger.warn("Could not destroy KeyExpirationEventMessageListener", e); + } + } + } - if (this.expirationListener.get() != null) { - this.expirationListener.get().destroy(); + messageListenerContainer.stop(); + state.set(State.STOPPED); } + } + + public void destroy() throws Exception { + + stop(); if (this.managedListenerContainer && this.messageListenerContainer != null) { this.messageListenerContainer.destroy(); this.messageListenerContainer = null; } + + this.state.set(State.DESTROYED); } @Override @@ -729,15 +786,13 @@ private void initMessageListenerContainer() { this.messageListenerContainer = new RedisMessageListenerContainer(); this.messageListenerContainer.setConnectionFactory(((RedisTemplate) redisOps).getConnectionFactory()); this.messageListenerContainer.afterPropertiesSet(); - this.messageListenerContainer.start(); } - private void initKeyExpirationListener() { + private void initKeyExpirationListener(RedisMessageListenerContainer messageListenerContainer) { if (this.expirationListener.get() == null) { - - MappingExpirationListener listener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, - this.converter); + MappingExpirationListener listener = new MappingExpirationListener(messageListenerContainer, this.redisOps, + this.converter, this.shadowCopy); listener.setKeyspaceNotificationsConfigParameter(keyspaceNotificationsConfigParameter); @@ -763,16 +818,18 @@ static class MappingExpirationListener extends KeyExpirationEventMessageListener private final RedisOperations ops; private final RedisConverter converter; + private final ShadowCopy shadowCopy; /** * Creates new {@link MappingExpirationListener}. */ MappingExpirationListener(RedisMessageListenerContainer listenerContainer, RedisOperations ops, - RedisConverter converter) { + RedisConverter converter, ShadowCopy shadowCopy) { super(listenerContainer); this.ops = ops; this.converter = converter; + this.shadowCopy = shadowCopy; } @Override @@ -783,23 +840,7 @@ public void onMessage(Message message, @Nullable byte[] pattern) { } byte[] key = message.getBody(); - - byte[] phantomKey = ByteUtils.concat(key, - converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class)); - - Map hash = ops.execute((RedisCallback>) connection -> { - - Map phantomValue = connection.hGetAll(phantomKey); - - if (!CollectionUtils.isEmpty(phantomValue)) { - connection.del(phantomKey); - } - - return phantomValue; - }); - - Object value = CollectionUtils.isEmpty(hash) ? null : converter.read(Object.class, new RedisData(hash)); - + Object value = readShadowCopyIfEnabled(key); byte[] channelAsBytes = message.getChannel(); String channel = !ObjectUtils.isEmpty(channelAsBytes) @@ -821,6 +862,35 @@ public void onMessage(Message message, @Nullable byte[] pattern) { private boolean isKeyExpirationMessage(Message message) { return BinaryKeyspaceIdentifier.isValid(message.getBody()); } + + @Nullable + private Object readShadowCopyIfEnabled(byte[] key) { + + if (shadowCopy == ShadowCopy.OFF) { + return null; + } + return readShadowCopy(key); + } + + @Nullable + private Object readShadowCopy(byte[] key) { + + byte[] phantomKey = ByteUtils.concat(key, + converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class)); + + Map hash = ops.execute((RedisCallback>) connection -> { + + Map phantomValue = connection.hGetAll(phantomKey); + + if (!CollectionUtils.isEmpty(phantomValue)) { + connection.del(phantomKey); + } + + return phantomValue; + }); + + return CollectionUtils.isEmpty(hash) ? null : converter.read(Object.class, new RedisData(hash)); + } } private boolean keepShadowCopy() { diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java index edbe5c2ff6..4e1454f533 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisKeyspaceEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java index 3a5b6b342f..aff2138cb0 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyspaceEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 7128522447..4ea682d900 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -25,10 +25,12 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.stream.ObjectRecord; import org.springframework.data.redis.core.query.SortQuery; import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.hash.HashMapper; import org.springframework.data.redis.serializer.RedisSerializer; @@ -47,6 +49,7 @@ * @author Todd Merrill * @author Chen Li * @author Vedran Pavic + * @author Marcin Grzejszczak */ public interface RedisOperations { @@ -61,7 +64,7 @@ public interface RedisOperations { * * @param return type * @param action callback object that specifies the Redis action. Must not be {@literal null}. - * @return a result object returned by the action or {@literal null} + * @return result of the given {@link RedisCallback#doInRedis(RedisConnection)} invocation. */ @Nullable T execute(RedisCallback action); @@ -72,7 +75,7 @@ public interface RedisOperations { * * @param return type * @param session session callback. Must not be {@literal null}. - * @return result object returned by the action or {@literal null} + * @return result of the given {@link SessionCallback#execute(RedisOperations)} invocation. */ @Nullable T execute(SessionCallback session); @@ -83,7 +86,9 @@ public interface RedisOperations { * serializers to deserialize results * * @param action callback object to execute - * @return list of objects returned by the pipeline + * @return pipeline results of the given {@link RedisCallback#doInRedis(RedisConnection)} invocation. Results are + * collected from {@link RedisConnection} calls, {@link RedisCallback#doInRedis(RedisConnection)} itself must + * return {@literal null}. */ List executePipelined(RedisCallback action); @@ -94,7 +99,9 @@ public interface RedisOperations { * @param action callback object to execute * @param resultSerializer The Serializer to use for individual values or Collections of values. If any returned * values are hashes, this serializer will be used to deserialize both the key and value - * @return list of objects returned by the pipeline + * @return pipeline results of the given {@link RedisCallback#doInRedis(RedisConnection)} invocation. Results are + * collected from {@link RedisConnection} calls, {@link RedisCallback#doInRedis(RedisConnection)} itself must + * return {@literal null}. */ List executePipelined(RedisCallback action, RedisSerializer resultSerializer); @@ -103,7 +110,9 @@ public interface RedisOperations { * callback cannot return a non-null value as it gets overwritten by the pipeline. * * @param session Session callback - * @return list of objects returned by the pipeline + * @return pipeline results of the given {@link SessionCallback#execute(RedisOperations)} invocation. Results are + * collected from {@link RedisOperations} calls, {@link SessionCallback#execute(RedisOperations)} itself must + * return {@literal null}. */ List executePipelined(SessionCallback session); @@ -114,7 +123,9 @@ public interface RedisOperations { * * @param session Session callback * @param resultSerializer - * @return list of objects returned by the pipeline + * @return pipeline results of the given {@link SessionCallback#execute(RedisOperations)} invocation. Results are + * collected from {@link RedisOperations} calls, {@link SessionCallback#execute(RedisOperations)} itself must + * return {@literal null}. */ List executePipelined(SessionCallback session, RedisSerializer resultSerializer); @@ -151,7 +162,7 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer * to free resources after use. * * @param callback must not be {@literal null}. - * @return + * @return the {@link Object result} of the operation performed in the callback or {@literal null}. * @since 1.8 */ @Nullable @@ -167,7 +178,7 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer * @param sourceKey must not be {@literal null}. * @param targetKey must not be {@literal null}. * @param replace whether the key was copied. {@literal null} when used in pipeline / transaction. - * @return + * @return {@code true} when copied successfully or {@literal null} when used in pipeline / transaction. * @see Redis Documentation: COPY * @since 2.6 */ @@ -178,7 +189,7 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer * Determine if given {@code key} exists. * * @param key must not be {@literal null}. - * @return + * @return {@literal true} if key exists. {@literal null} when used in pipeline / transaction. * @see Redis Documentation: EXISTS */ @Nullable @@ -357,11 +368,39 @@ default Boolean expireAt(K key, Instant expireAt) { return expireAt(key, Date.from(expireAt)); } + /** + * Set the expiration for given {@code key}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options must not be {@literal null}. + * @return changes to the expiry. {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException any of the required arguments is {@literal null}. + * @see Redis Documentation: EXPIRE + * @see Redis Documentation: PEXPIRE + * @see Redis Documentation: EXPIREAT + * @see Redis Documentation: PEXPIREAT + * @see Redis Documentation: PERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges.ExpiryChangeState expire(K key, Expiration expiration, ExpirationOptions options); + + /** + * Returns a bound operations object to perform expiration operations on the bound key. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundKeyExpirationOperations expiration(K key) { + return new DefaultBoundKeyExpirationOperations<>(this, key); + } + /** * Remove the expiration from given {@code key}. * * @param key must not be {@literal null}. - * @return {@literal null} when used in pipeline / transaction. + * @return {@code true} when persisted successfully or {@literal null} when used in pipeline / transaction. * @see Redis Documentation: PERSIST */ @Nullable @@ -650,7 +689,7 @@ default void restore(K key, byte[] value, long timeToLive, TimeUnit unit) { BoundHashOperations boundHashOps(K key); /** - * @return + * @return never {@literal null}. * @since 1.5 */ HyperLogLogOperations opsForHyperLogLog(); diff --git a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java index 07b946d2b9..070e04d4ac 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java +++ b/src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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.util.Map; import java.util.Set; +import org.springframework.core.convert.ConversionService; import org.springframework.data.geo.Circle; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; @@ -81,8 +82,8 @@ private RedisQueryEngine(CriteriaAccessor criteriaAccessor, @Override @SuppressWarnings("unchecked") - public List execute(RedisOperationChain criteria, Comparator sort, long offset, int rows, - String keyspace, Class type) { + public List execute(RedisOperationChain criteria, Comparator sort, long offset, int rows, String keyspace, + Class type) { List result = doFind(criteria, offset, rows, keyspace, type); if (sort != null) { @@ -199,8 +200,7 @@ private List findKeys(RedisOperationChain criteria, int rows, String key } @Override - public List execute(RedisOperationChain criteria, Comparator sort, long offset, int rows, - String keyspace) { + public List execute(RedisOperationChain criteria, Comparator sort, long offset, int rows, String keyspace) { return execute(criteria, sort, offset, rows, keyspace, Object.class); } @@ -229,14 +229,13 @@ public long count(RedisOperationChain criteria, String keyspace) { private byte[][] keys(String prefix, Collection source) { + ConversionService conversionService = getRequiredAdapter().getConverter().getConversionService(); byte[][] keys = new byte[source.size()][]; int i = 0; for (PathAndValue pathAndValue : source) { - byte[] convertedValue = getRequiredAdapter().getConverter().getConversionService() - .convert(pathAndValue.getFirstValue(), byte[].class); - byte[] fullPath = getRequiredAdapter().getConverter().getConversionService() - .convert(prefix + pathAndValue.getPath() + ":", byte[].class); + byte[] convertedValue = conversionService.convert(pathAndValue.getFirstValue(), byte[].class); + byte[] fullPath = conversionService.convert(prefix + pathAndValue.getPath() + ":", byte[].class); keys[i] = ByteUtils.concat(fullPath, convertedValue); i++; diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 93ee4ca0de..c80f555ee0 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -33,6 +33,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisKeyCommands; @@ -46,6 +47,7 @@ import org.springframework.data.redis.core.script.DefaultScriptExecutor; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.core.script.ScriptExecutor; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.hash.HashMapper; import org.springframework.data.redis.hash.ObjectHashMapper; @@ -66,10 +68,11 @@ * Redis store. By default, it uses Java serialization for its objects (through {@link JdkSerializationRedisSerializer} * ). For String intensive operations consider the dedicated {@link StringRedisTemplate}. *

    - * The central method is execute, supporting Redis access code implementing the {@link RedisCallback} interface. It - * provides {@link RedisConnection} handling such that neither the {@link RedisCallback} implementation nor the calling - * code needs to explicitly care about retrieving/closing Redis connections, or handling Connection lifecycle - * exceptions. For typical single step actions, there are various convenience methods. + * The central method is {@link #execute(RedisCallback)}, supporting Redis access code implementing the + * {@link RedisCallback} interface. It provides {@link RedisConnection} handling such that neither the + * {@link RedisCallback} implementation nor the calling code needs to explicitly care about retrieving/closing Redis + * connections, or handling Connection lifecycle exceptions. For typical single step actions, there are various + * convenience methods. *

    * Once configured, this class is thread-safe. *

    @@ -116,11 +119,12 @@ public class RedisTemplate extends RedisAccessor implements RedisOperation ObjectHashMapper.getSharedInstance()); private final ZSetOperations zSetOps = new DefaultZSetOperations<>(this); private final GeoOperations geoOps = new DefaultGeoOperations<>(this); + private final HashOperations hashOps = new DefaultHashOperations<>(this); private final HyperLogLogOperations hllOps = new DefaultHyperLogLogOperations<>(this); private final ClusterOperations clusterOps = new DefaultClusterOperations<>(this); /** - * Constructs a new RedisTemplate instance. + * Constructs a new {@code RedisTemplate} instance. */ public RedisTemplate() {} @@ -159,46 +163,59 @@ public void afterPropertiesSet() { } /** - * Returns whether to expose the native Redis connection to RedisCallback code, or rather a connection proxy (the - * default). + * Returns whether the underlying RedisConnection should be directly exposed to the RedisCallback code, or rather a + * connection proxy (default behavior). * - * @return whether to expose the native Redis connection or not + * @return {@literal true} to expose the native Redis connection or {@literal false} to provide a proxied connection + * to RedisCallback code. */ public boolean isExposeConnection() { return exposeConnection; } /** - * Sets whether to expose the Redis connection to {@link RedisCallback} code. Default is "false": a proxy will be - * returned, suppressing {@code quit} and {@code disconnect} calls. + * Sets whether the underlying RedisConnection should be directly exposed to the RedisCallback code. By default, the + * connection is not exposed, and a proxy is used instead. This proxy suppresses potentially disruptive operations, + * such as {@code quit} and {@code disconnect} commands, ensuring that the connection remains stable during the + * callback execution. Defaults to proxy use. * - * @param exposeConnection + * @param exposeConnection {@literal true} to expose the actual Redis connection to RedisCallback code, allowing full + * access to Redis commands, including quit and disconnect. {@literal false} to proxy connections that + * suppress the quit and disconnect commands, protecting the connection from being inadvertently closed + * during callback execution. */ public void setExposeConnection(boolean exposeConnection) { this.exposeConnection = exposeConnection; } /** - * @return Whether or not the default serializer should be used. If not, any serializers not explicitly set will - * remain null and values will not be serialized or deserialized. + * Returns whether the default serializer should be used or not. + * + * @return {@literal true} if the default serializer should be used; {@literal false} otherwise. */ public boolean isEnableDefaultSerializer() { return enableDefaultSerializer; } /** - * @param enableDefaultSerializer Whether or not the default serializer should be used. If not, any serializers not - * explicitly set will remain null and values will not be serialized or deserialized. + * Configure whether the default serializer should be used or not. If the default serializer is enabled, the template + * will use it to serialize and deserialize values. However, if the default serializer is disabled , any serializers + * that have not been explicitly set will remain {@literal null}, and their corresponding values will neither be + * serialized nor deserialized. Defaults to {@literal true}. + * + * @param enableDefaultSerializer {@literal true} if the default serializer should be used; {@literal false} + * otherwise. */ public void setEnableDefaultSerializer(boolean enableDefaultSerializer) { this.enableDefaultSerializer = enableDefaultSerializer; } /** - * If set to {@code true} {@link RedisTemplate} will participate in ongoing transactions using - * {@literal MULTI...EXEC|DISCARD} to keep track of operations. + * Sets whether this template participates in ongoing transactions using {@literal MULTI...EXEC|DISCARD} to keep track + * of operations. * - * @param enableTransactionSupport whether to participate in ongoing transactions. + * @param enableTransactionSupport {@literal true}to participate in ongoing transactions; {@literal false} to not + * track transactions. * @since 1.3 * @see RedisConnectionUtils#getConnection(RedisConnectionFactory, boolean) * @see TransactionSynchronizationManager#isActualTransactionActive() @@ -208,7 +225,7 @@ public void setEnableTransactionSupport(boolean enableTransactionSupport) { } /** - * Set the {@link ClassLoader} to be used for the default {@link JdkSerializationRedisSerializer} in case no other + * Sets the {@link ClassLoader} to be used for the default {@link JdkSerializationRedisSerializer} in case no other * {@link RedisSerializer} is explicitly set as the default one. * * @param classLoader can be {@literal null}. @@ -223,7 +240,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { /** * Returns the default serializer used by this template. * - * @return template default serializer + * @return template default serializer. */ @Nullable public RedisSerializer getDefaultSerializer() { @@ -235,7 +252,7 @@ public RedisSerializer getDefaultSerializer() { * {@link #setStringSerializer(RedisSerializer)}) are initialized to this value unless explicitly set. Defaults to * {@link JdkSerializationRedisSerializer}. * - * @param serializer default serializer to use + * @param serializer default serializer to use. */ public void setDefaultSerializer(RedisSerializer serializer) { this.defaultSerializer = serializer; @@ -696,6 +713,16 @@ public Boolean expireAt(K key, final Date date) { }); } + @Nullable + @Override + public ExpireChanges.ExpiryChangeState expire(K key, Expiration expiration, ExpirationOptions options) { + + byte[] rawKey = rawKey(key); + Boolean raw = doWithKeys(connection -> connection.applyExpiration(rawKey, expiration, options)); + + return raw != null ? ExpireChanges.ExpiryChangeState.of(raw) : null; + } + @Override public Boolean persist(K key) { @@ -966,18 +993,21 @@ public GeoOperations opsForGeo() { } @Override + @SuppressWarnings("unchecked") public BoundGeoOperations boundGeoOps(K key) { return boundOperations.createProxy(BoundGeoOperations.class, key, DataType.ZSET, this, RedisOperations::opsForGeo); } @Override + @SuppressWarnings("unchecked") public BoundHashOperations boundHashOps(K key) { return boundOperations.createProxy(BoundHashOperations.class, key, DataType.HASH, this, it -> it.opsForHash()); } @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) public HashOperations opsForHash() { - return new DefaultHashOperations<>(this); + return (HashOperations) hashOps; } @Override @@ -991,12 +1021,14 @@ public ListOperations opsForList() { } @Override + @SuppressWarnings("unchecked") public BoundListOperations boundListOps(K key) { return boundOperations.createProxy(BoundListOperations.class, key, DataType.LIST, this, RedisOperations::opsForList); } @Override + @SuppressWarnings("unchecked") public BoundSetOperations boundSetOps(K key) { return boundOperations.createProxy(BoundSetOperations.class, key, DataType.SET, this, RedisOperations::opsForSet); } @@ -1007,6 +1039,7 @@ public SetOperations opsForSet() { } @Override + @SuppressWarnings("unchecked") public StreamOperations opsForStream() { return (StreamOperations) streamOps; } @@ -1017,11 +1050,13 @@ public StreamOperations opsForStream(HashMapper BoundStreamOperations boundStreamOps(K key) { return boundOperations.createProxy(BoundStreamOperations.class, key, DataType.STREAM, this, it -> opsForStream()); } @Override + @SuppressWarnings("unchecked") public BoundValueOperations boundValueOps(K key) { return boundOperations.createProxy(BoundValueOperations.class, key, DataType.STRING, this, RedisOperations::opsForValue); @@ -1033,6 +1068,7 @@ public ValueOperations opsForValue() { } @Override + @SuppressWarnings("unchecked") public BoundZSetOperations boundZSetOps(K key) { return boundOperations.createProxy(BoundZSetOperations.class, key, DataType.ZSET, this, RedisOperations::opsForZSet); diff --git a/src/main/java/org/springframework/data/redis/core/ScanCursor.java b/src/main/java/org/springframework/data/redis/core/ScanCursor.java index 8c94ff726b..354e0cfcc9 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanCursor.java +++ b/src/main/java/org/springframework/data/redis/core/ScanCursor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -86,7 +86,7 @@ public ScanCursor(CursorId cursorId) { * Crates new {@link ScanCursor} * * @param cursorId the cursor Id. - * @param options Defaulted to {@link ScanOptions#NONE} if {@code null}. + * @param options Defaulted to {@link ScanOptions#NONE} if {@literal null}. * @deprecated since 3.3.0 - Use {@link ScanCursor#ScanCursor(CursorId, ScanOptions)} instead. */ @Deprecated(since = "3.3.0") @@ -98,7 +98,7 @@ public ScanCursor(long cursorId, @Nullable ScanOptions options) { * Crates new {@link ScanCursor} * * @param cursorId the cursor Id. - * @param options Defaulted to {@link ScanOptions#NONE} if {@code null}. + * @param options Defaulted to {@link ScanOptions#NONE} if {@literal null}. * @since 3.3.0 */ public ScanCursor(CursorId cursorId, @Nullable ScanOptions options) { @@ -125,7 +125,7 @@ private void scan(CursorId cursorId) { /** * Performs the actual scan command using the native client implementation. The given {@literal options} are never - * {@code null}. + * {@literal null}. * * @param cursorId * @param options @@ -139,7 +139,7 @@ protected ScanIteration doScan(long cursorId, ScanOptions options) { /** * Performs the actual scan command using the native client implementation. The given {@literal options} are never - * {@code null}. + * {@literal null}. * * @param cursorId * @param options diff --git a/src/main/java/org/springframework/data/redis/core/ScanIteration.java b/src/main/java/org/springframework/data/redis/core/ScanIteration.java index 1be8cf8771..3e0b5a5929 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanIteration.java +++ b/src/main/java/org/springframework/data/redis/core/ScanIteration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/ScanOptions.java b/src/main/java/org/springframework/data/redis/core/ScanOptions.java index 959aa9aaea..b7520703f6 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanOptions.java +++ b/src/main/java/org/springframework/data/redis/core/ScanOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/redis/core/SessionCallback.java b/src/main/java/org/springframework/data/redis/core/SessionCallback.java index 0d41a5299f..42a53a068f 100644 --- a/src/main/java/org/springframework/data/redis/core/SessionCallback.java +++ b/src/main/java/org/springframework/data/redis/core/SessionCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/SetOperations.java b/src/main/java/org/springframework/data/redis/core/SetOperations.java index 00471359d3..b1eddb91da 100644 --- a/src/main/java/org/springframework/data/redis/core/SetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/SetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/SetOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/SetOperationsEditor.java index cad663aff8..a00ce61de0 100644 --- a/src/main/java/org/springframework/data/redis/core/SetOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/SetOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/StreamObjectMapper.java b/src/main/java/org/springframework/data/redis/core/StreamObjectMapper.java index d8e340f330..4b72930993 100644 --- a/src/main/java/org/springframework/data/redis/core/StreamObjectMapper.java +++ b/src/main/java/org/springframework/data/redis/core/StreamObjectMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/core/StreamOperations.java b/src/main/java/org/springframework/data/redis/core/StreamOperations.java index 4636c5bcf6..59a0647f29 100644 --- a/src/main/java/org/springframework/data/redis/core/StreamOperations.java +++ b/src/main/java/org/springframework/data/redis/core/StreamOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -25,6 +25,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisStreamCommands.XClaimOptions; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.stream.ByteRecord; import org.springframework.data.redis.connection.stream.Consumer; import org.springframework.data.redis.connection.stream.MapRecord; @@ -53,6 +54,7 @@ * @author Dengliming * @author Marcin Zielinski * @author John Blum + * @author jinkshower * @since 2.2 */ public interface StreamOperations extends HashMapperProvider { @@ -95,6 +97,53 @@ default Long acknowledge(String group, Record record) { return acknowledge(record.getRequiredStream(), group, record.getId()); } + /** + * Append a record to the stream {@code key} with the specified options. + * + * @param key the stream key. + * @param content record content as Map. + * @param xAddOptions additional parameters for the {@literal XADD} call. + * @return the record Id. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: XADD + * @since 3.4 + */ + @SuppressWarnings("unchecked") + @Nullable + default RecordId add(K key, Map content, XAddOptions xAddOptions) { + return add(StreamRecords.newRecord().in(key).ofMap(content), xAddOptions); + } + + /** + * Append a record, backed by a {@link Map} holding the field/value pairs, to the stream with the specified options. + * + * @param record the record to append. + * @param xAddOptions additional parameters for the {@literal XADD} call. + * @return the record Id. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: XADD + * @since 3.4 + */ + @SuppressWarnings("unchecked") + @Nullable + default RecordId add(MapRecord record, XAddOptions xAddOptions) { + return add((Record) record, xAddOptions); + } + + /** + * Append the record, backed by the given value, to the stream with the specified options. + * The value will be hashed and serialized. + * + * @param record must not be {@literal null}. + * @param xAddOptions parameters for the {@literal XADD} call. Must not be {@literal null}. + * @return the record Id. {@literal null} when used in pipeline / transaction. + * @see MapRecord + * @see ObjectRecord + * @see Redis Documentation: XADD + * @since 3.4 + */ + @SuppressWarnings("unchecked") + @Nullable + RecordId add(Record record, XAddOptions xAddOptions); + /** * Append a record to the stream {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/core/StreamOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/StreamOperationsEditor.java index 5776342b2c..1dbb0b6bad 100644 --- a/src/main/java/org/springframework/data/redis/core/StreamOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/StreamOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/StringRedisTemplate.java b/src/main/java/org/springframework/data/redis/core/StringRedisTemplate.java index 072872be57..5661d4f714 100644 --- a/src/main/java/org/springframework/data/redis/core/StringRedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/StringRedisTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/TimeToLive.java b/src/main/java/org/springframework/data/redis/core/TimeToLive.java index bcc148c608..c3f568a947 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeToLive.java +++ b/src/main/java/org/springframework/data/redis/core/TimeToLive.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/TimeToLiveAccessor.java b/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java index 0b90a8c973..54ecf92735 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java +++ b/src/main/java/org/springframework/data/redis/core/TimeToLiveAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/TimeoutUtils.java b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java index 064fda4a8e..c46e8478d6 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java +++ b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -35,7 +35,11 @@ public abstract class TimeoutUtils { * @since 2.1 */ public static boolean hasMillis(Duration duration) { - return duration.toMillis() % 1000 != 0; + return containsSplitSecond(duration.toMillis()); + } + + public static boolean containsSplitSecond(long millis) { + return millis % 1000 != 0; } /** diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 546a83f8ff..84ce00f385 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -32,6 +32,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Jiahe Cai + * @author Marcin Grzejszczak */ public interface ValueOperations { @@ -44,6 +45,35 @@ public interface ValueOperations { */ void set(K key, V value); + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old string stored at key, or + * {@literal null} if key did not exist. An error is returned and SET aborted if the value stored at key is not a + * string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param timeout the key expiration timeout. + * @param unit must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: SET + * @since 3.5 + */ + V setGet(K key, V value, long timeout, TimeUnit unit); + + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old string stored at key, or + * {@literal null} if key did not exist. An error is returned and SET aborted if the value stored at key is not a + * string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param duration expiration duration + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: SET + * @since 3.5 + */ + V setGet(K key, V value, Duration duration); + /** * Set the {@code value} and expiration {@code timeout} for {@code key}. * @@ -62,7 +92,7 @@ public interface ValueOperations { * @param value must not be {@literal null}. * @param timeout must not be {@literal null}. * @throws IllegalArgumentException if either {@code key}, {@code value} or {@code timeout} is not present. - * @see Redis Documentation: SETEX + * @see Redis Documentation: SET * @since 2.1 */ default void set(K key, V value, Duration timeout) { @@ -82,7 +112,7 @@ default void set(K key, V value, Duration timeout) { * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: SETNX + * @see Redis Documentation: SET */ @Nullable Boolean setIfAbsent(K key, V value); @@ -264,7 +294,7 @@ default Boolean setIfPresent(K key, V value, Duration timeout) { /** * Get multiple {@code keys}. Values are in the order of the requested keys Absent field values are represented using - * {@code null} in the resulting {@link List}. + * {@literal null} in the resulting {@link List}. * * @param keys must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/ValueOperationsEditor.java index 1eaf8949fa..dd87ce60f4 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/ZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java index e20f560410..263346fe5f 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/ZSetOperationsEditor.java b/src/main/java/org/springframework/data/redis/core/ZSetOperationsEditor.java index 124eccfd07..23526dc956 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperationsEditor.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperationsEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/core/convert/BinaryConverters.java b/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java index 402a68d7ac..0befb22a44 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java +++ b/src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -289,7 +289,7 @@ public Date convert(byte[] source) { } catch (ParseException ignore) { } - throw new IllegalArgumentException(String.format("Cannot parse date out of %s", Arrays.toString(source))); + throw new IllegalArgumentException("Cannot parse date out of %s".formatted(Arrays.toString(source))); } } diff --git a/src/main/java/org/springframework/data/redis/core/convert/Bucket.java b/src/main/java/org/springframework/data/redis/core/convert/Bucket.java index 3599e8b5ee..5b832b2d62 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/Bucket.java +++ b/src/main/java/org/springframework/data/redis/core/convert/Bucket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,10 +108,10 @@ public byte[] get(String path) { } /** - * Return whether {@code path} is associated with a non-{@code null} value. + * Return whether {@code path} is associated with a non-{@literal null} value. * * @param path must not be {@literal null} or {@link String#isEmpty()}. - * @return {@literal true} if the {@code path} is associated with a non-{@code null} value. + * @return {@literal true} if the {@code path} is associated with a non-{@literal null} value. * @since 2.5 */ public boolean hasValue(String path) { @@ -178,7 +178,7 @@ public Bucket extract(String prefix) { * Get all the keys matching a given path. * * @param path the path to look for. Can be {@literal null}. - * @return all keys if path is {@null} or empty. + * @return all keys if path is {@literal null} or empty. */ public Set extractAllKeysFor(String path) { diff --git a/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java index fd6d702cd0..13581c45dc 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/CompositeIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/DefaultRedisTypeMapper.java b/src/main/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapper.java index 5896c6ff43..9b538ec135 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapper.java +++ b/src/main/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/convert/GeoIndexedPropertyValue.java b/src/main/java/org/springframework/data/redis/core/convert/GeoIndexedPropertyValue.java index 979d576a08..d45cd31a16 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/GeoIndexedPropertyValue.java +++ b/src/main/java/org/springframework/data/redis/core/convert/GeoIndexedPropertyValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,11 +74,10 @@ public boolean equals(@Nullable Object o) { return true; } - if (!(o instanceof GeoIndexedPropertyValue)) { + if (!(o instanceof GeoIndexedPropertyValue that)) { return false; } - GeoIndexedPropertyValue that = (GeoIndexedPropertyValue) o; if (!ObjectUtils.nullSafeEquals(keyspace, that.keyspace)) { return false; } diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java index 95c63c2992..b06e82587a 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/IndexedData.java b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java index e6c0fce9e5..298c48cf6b 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexedData.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,4 +38,13 @@ public interface IndexedData { */ String getKeyspace(); + /** + * Return the key prefix for usage in Redis. + * + * @return concatenated form of the keyspace and the index name. + * @since 3.3.4 + */ + default String getKeyPrefix() { + return getKeyspace() + ":" + getIndexName(); + } } diff --git a/src/main/java/org/springframework/data/redis/core/convert/IndexedDataFactoryProvider.java b/src/main/java/org/springframework/data/redis/core/convert/IndexedDataFactoryProvider.java index c9521d0602..3c4a51bbf7 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/IndexedDataFactoryProvider.java +++ b/src/main/java/org/springframework/data/redis/core/convert/IndexedDataFactoryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/Jsr310Converters.java b/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java index d4e874e9e9..225e1ef85b 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java +++ b/src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/KeyspaceConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java index d3fcd1b5cb..5bb5b0a470 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/MappingConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java index a938bbeafa..61d79cf68f 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index e9e1a9f312..1cea9a221a 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -540,8 +540,8 @@ private void writePartialPropertyUpdate(PartialUpdate update, PropertyUpdate map.put(((Entry) pUpdate.getValue()).getKey(), ((Entry) pUpdate.getValue()).getValue()); } else { throw new MappingException( - String.format("Cannot set update value for map property '%s' to '%s'; Please use a Map or Map.Entry", - pUpdate.getPropertyPath(), pUpdate.getValue())); + ("Cannot set update value for map property '%s' to '%s';" + " Please use a Map or Map.Entry") + .formatted(pUpdate.getPropertyPath(), pUpdate.getValue())); } writeMap(entity.getKeySpace(), pUpdate.getPropertyPath(), targetProperty.getMapValueType(), map, sink); @@ -601,8 +601,7 @@ private void writeInternal(@Nullable String keyspace, String path, @Nullable Obj } else { if (!ClassUtils.isAssignable(typeHint.getType(), value.getClass())) { - throw new MappingException( - String.format(INVALID_TYPE_ASSIGNMENT, value.getClass(), path, typeHint.getType())); + throw new MappingException(INVALID_TYPE_ASSIGNMENT.formatted(value.getClass(), path, typeHint.getType())); } writeToBucket(path, value, sink, typeHint.getType()); } @@ -751,7 +750,7 @@ private void writeCollection(@Nullable String keyspace, String path, @Nullable I if (!ClassUtils.isAssignable(typeHint.getType(), value.getClass())) { throw new MappingException( - String.format(INVALID_TYPE_ASSIGNMENT, value.getClass(), currentPath, typeHint.getType())); + INVALID_TYPE_ASSIGNMENT.formatted(value.getClass(), currentPath, typeHint.getType())); } if (customConversions.hasCustomWriteTarget(value.getClass())) { @@ -794,7 +793,7 @@ private void writeToBucket(String path, @Nullable Object value, RedisData sink, sink.getBucket().put(path, toBytes(value)); } else { throw new IllegalArgumentException( - String.format("Cannot convert value '%s' of type %s to bytes", value, value.getClass())); + "Cannot convert value '%s' of type %s to bytes".formatted(value, value.getClass())); } } } @@ -855,7 +854,7 @@ private void writeMap(@Nullable String keyspace, String path, Class mapValueT if (!ClassUtils.isAssignable(mapValueType, entry.getValue().getClass())) { throw new MappingException( - String.format(INVALID_TYPE_ASSIGNMENT, entry.getValue().getClass(), currentPath, mapValueType)); + INVALID_TYPE_ASSIGNMENT.formatted(entry.getValue().getClass(), currentPath, mapValueType)); } if (customConversions.hasCustomWriteTarget(entry.getValue().getClass())) { @@ -945,8 +944,7 @@ private Object extractMapKeyForPath(String path, String key, Class targetType Matcher matcher = pattern.matcher(key); if (!matcher.find()) { - throw new IllegalArgumentException( - String.format("Cannot extract map value for key '%s' in path '%s'.", key, path)); + throw new IllegalArgumentException("Cannot extract map value for key '%s' in path '%s'".formatted(key, path)); } Object mapKey = matcher.group(2); @@ -1224,7 +1222,7 @@ private KeyspaceIdentifier(String keyspace, String id, boolean phantomKey) { */ public static KeyspaceIdentifier of(String key) { - Assert.isTrue(isValid(key), String.format("Invalid key %s", key)); + Assert.isTrue(isValid(key), () -> "Invalid key %s".formatted(key)); boolean phantomKey = key.endsWith(PHANTOM_SUFFIX); int keyspaceEndIndex = key.indexOf(DELIMITER); @@ -1304,7 +1302,7 @@ private BinaryKeyspaceIdentifier(byte[] keyspace, byte[] id, boolean phantomKey) */ public static BinaryKeyspaceIdentifier of(byte[] key) { - Assert.isTrue(isValid(key), String.format("Invalid key %s", new String(key))); + Assert.isTrue(isValid(key), () -> "Invalid key %s".formatted(new String(key))); boolean phantomKey = ByteUtils.startsWith(key, PHANTOM_SUFFIX, key.length - PHANTOM_SUFFIX.length); diff --git a/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java index f23873ee9d..00c1964217 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/PathIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/RedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java index 998f223233..dbbb692472 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/RedisCustomConversions.java b/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java index 5f097242a9..a58f3c9b23 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/convert/RedisData.java b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java index a571c205f9..1b68ebaefd 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisData.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisData.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/RedisTypeMapper.java b/src/main/java/org/springframework/data/redis/core/convert/RedisTypeMapper.java index 8f8ba965ab..6babc758a4 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisTypeMapper.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/convert/ReferenceResolver.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java index a68653200c..0c2bc6b37f 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/ReferenceResolverImpl.java b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java index 19ad617332..32d5b7f496 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java +++ b/src/main/java/org/springframework/data/redis/core/convert/ReferenceResolverImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/RemoveIndexedData.java b/src/main/java/org/springframework/data/redis/core/convert/RemoveIndexedData.java index fde0d7d740..d8d5820c96 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RemoveIndexedData.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RemoveIndexedData.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/SimpleIndexedPropertyValue.java b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java index f218e7d07a..27d8f21612 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java +++ b/src/main/java/org/springframework/data/redis/core/convert/SimpleIndexedPropertyValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/SpelIndexResolver.java b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java index a386a29cdc..4b5f8418ae 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java +++ b/src/main/java/org/springframework/data/redis/core/convert/SpelIndexResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/ConfigurableIndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java index d97a686caa..4c22120359 100644 --- a/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java +++ b/src/main/java/org/springframework/data/redis/core/index/ConfigurableIndexDefinitionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/GeoIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java index 00a7edc640..9c2af6ca81 100644 --- a/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/GeoIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,8 +66,8 @@ public Point convert(@Nullable Object source) { } throw new IllegalArgumentException( - String.format("Cannot convert %s to %s; GeoIndexed property needs to be of type Point or GeoLocation", - source.getClass(), Point.class)); + ("Cannot convert %s to %s; GeoIndexed property needs to be of type Point" + " or GeoLocation") + .formatted(source.getClass(), Point.class)); } } } diff --git a/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java b/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java index 185b7bd19d..dd6c353abf 100644 --- a/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java +++ b/src/main/java/org/springframework/data/redis/core/index/GeoIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexConfiguration.java b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java index 12b55bbaf5..cb226bbaba 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java index 86151fc07e..deb6775be8 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexDefinitionProvider.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java index e4d07547d4..df4ee0769d 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexDefinitionRegistry.java b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java index 8e723ad498..90a395307b 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexDefinitionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexValueTransformer.java b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java index 13d77f9ee7..127c3ed88a 100644 --- a/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java +++ b/src/main/java/org/springframework/data/redis/core/index/IndexValueTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/Indexed.java b/src/main/java/org/springframework/data/redis/core/index/Indexed.java index 15dc14d3c0..1099d28265 100644 --- a/src/main/java/org/springframework/data/redis/core/index/Indexed.java +++ b/src/main/java/org/springframework/data/redis/core/index/Indexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/index/PathBasedRedisIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java index 9526f279b3..3f8e90c883 100644 --- a/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/PathBasedRedisIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/RedisIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java index 122659a12c..9831e3b9a0 100644 --- a/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/RedisIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,10 +105,9 @@ public boolean equals(@Nullable Object obj) { if (obj == null) { return false; } - if (!(obj instanceof RedisIndexDefinition)) { + if (!(obj instanceof RedisIndexDefinition that)) { return false; } - RedisIndexDefinition that = (RedisIndexDefinition) obj; if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) { return false; diff --git a/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java index 5bd19d501d..75d7ba6163 100644 --- a/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/SimpleIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/SpelIndexDefinition.java b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java index 8501e3b1b9..c346945686 100644 --- a/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java +++ b/src/main/java/org/springframework/data/redis/core/index/SpelIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/mapping/BasicRedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java index 7c68335874..28eede0b9d 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,11 +57,6 @@ public TimeToLiveAccessor getTimeToLiveAccessor() { return this.timeToLiveAccessor; } - @Override - public boolean hasExplictTimeToLiveProperty() { - return getExplicitTimeToLiveProperty() != null; - } - @Override @Nullable public RedisPersistentProperty getExplicitTimeToLiveProperty() { @@ -89,16 +84,14 @@ protected RedisPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul boolean newIdPropertyIsExplicit = property.isAnnotationPresent(Id.class); if (currentIdPropertyIsExplicit && newIdPropertyIsExplicit) { - throw new MappingException(String.format( - "Attempt to add explicit id property %s but already have an property %s registered " - + "as explicit id; Check your mapping configuration", - property.getField(), currentIdProperty.getField())); + throw new MappingException(("Attempt to add explicit id property %s but already have a property %s" + + " registered as explicit id; Check your mapping configuration") + .formatted(property.getField(), currentIdProperty.getField())); } if (!currentIdPropertyIsExplicit && !newIdPropertyIsExplicit) { - throw new MappingException( - String.format("Attempt to add id property %s but already have an property %s registered " - + "as id; Check your mapping configuration", property.getField(), currentIdProperty.getField())); + throw new MappingException(("Attempt to add id property %s but already have a property %s registered as id;" + + " Check your mapping configuration").formatted(property.getField(), currentIdProperty.getField())); } if (newIdPropertyIsExplicit) { diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java index e623311ee9..fa445cbfa5 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -241,17 +241,15 @@ public Long getTimeToLive(Object source) { return TimeUnit.SECONDS.convert(timeout.longValue(), ttl.unit()); } } catch (IllegalAccessException ex) { - String message = String.format("Not allowed to access method '%s': %s", - timeoutMethod.getName(), ex.getMessage()); - throw new IllegalStateException(message, ex); + throw new IllegalStateException( + "Not allowed to access method '%s': %s".formatted(timeoutMethod.getName(), ex.getMessage()), ex); } catch (IllegalArgumentException ex) { - String message = String.format("Cannot invoke method '%s' without arguments: %s", - timeoutMethod.getName(), ex.getMessage()); - throw new IllegalStateException(message, ex); + throw new IllegalStateException( + "Cannot invoke method '%s' without arguments: %s".formatted(timeoutMethod.getName(), ex.getMessage()), + ex); } catch (InvocationTargetException ex) { - String message = String.format("Cannot access method '%s': %s", - timeoutMethod.getName(), ex.getMessage()); - throw new IllegalStateException(message, ex); + throw new IllegalStateException( + "Cannot access method '%s': %s".formatted(timeoutMethod.getName(), ex.getMessage()), ex); } } } diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java index a31eca7895..4ef2104351 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,8 +40,20 @@ public interface RedisPersistentEntity extends KeyValuePersistentEntity * Streams of methods returning {@code Mono} or {@code Flux} are terminated with * {@link org.springframework.dao.InvalidDataAccessApiUsageException} when - * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@code null} for a - * particular element as Reactive Streams prohibit the usage of {@code null} values. + * {@link org.springframework.data.redis.serializer.RedisElementReader#read(ByteBuffer)} returns {@literal null} for a + * particular element as Reactive Streams prohibit the usage of {@literal null} values. * * @author Mark Paluch * @author Christoph Strobl @@ -67,8 +68,22 @@ default Flux execute(RedisScript script, List keys) { * Executes the given {@link RedisScript} * * @param script The script to execute. Must not be {@literal null}. - * @param keys Any keys that need to be passed to the script. Must not be {@literal null}. - * @param args Any args that need to be passed to the script. Can be {@literal empty}. + * @param keys any keys that need to be passed to the script. Must not be {@literal null}. + * @param args any args that need to be passed to the script. Can be {@literal empty}. + * @return The return value of the script or {@link Flux#empty()} if {@link RedisScript#getResultType()} is + * {@literal null}, likely indicating a throw-away status reply (i.e. "OK") + * @since 3.4 + */ + default Flux execute(RedisScript script, List keys, Object... args) { + return execute(script, keys, Arrays.asList(args)); + } + + /** + * Executes the given {@link RedisScript} + * + * @param script The script to execute. Must not be {@literal null}. + * @param keys any keys that need to be passed to the script. Must not be {@literal null}. + * @param args any args that need to be passed to the script. Can be {@literal empty}. * @return The return value of the script or {@link Flux#empty()} if {@link RedisScript#getResultType()} is * {@literal null}, likely indicating a throw-away status reply (i.e. "OK") */ @@ -79,8 +94,8 @@ default Flux execute(RedisScript script, List keys) { * arguments and result. * * @param script The script to execute. must not be {@literal null}. - * @param keys Any keys that need to be passed to the script - * @param args Any args that need to be passed to the script + * @param keys any keys that need to be passed to the script. + * @param args any args that need to be passed to the script. * @param argsWriter The {@link RedisElementWriter} to use for serializing args. Must not be {@literal null}. * @param resultReader The {@link RedisElementReader} to use for serializing the script return value. Must not be * {@literal null}. diff --git a/src/main/java/org/springframework/data/redis/core/script/RedisScript.java b/src/main/java/org/springframework/data/redis/core/script/RedisScript.java index 67ec37ed5a..a7cc80f2c2 100644 --- a/src/main/java/org/springframework/data/redis/core/script/RedisScript.java +++ b/src/main/java/org/springframework/data/redis/core/script/RedisScript.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/script/ScriptExecutor.java b/src/main/java/org/springframework/data/redis/core/script/ScriptExecutor.java index b7b658a7e4..ba40f40f0d 100644 --- a/src/main/java/org/springframework/data/redis/core/script/ScriptExecutor.java +++ b/src/main/java/org/springframework/data/redis/core/script/ScriptExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -30,11 +30,11 @@ public interface ScriptExecutor { /** * Executes the given {@link RedisScript} * - * @param script The script to execute - * @param keys Any keys that need to be passed to the script - * @param args Any args that need to be passed to the script - * @return The return value of the script or null if {@link RedisScript#getResultType()} is null, likely indicating a - * throw-away status reply (i.e. "OK") + * @param script the script to execute. + * @param keys any keys that need to be passed to the script. + * @param args any args that need to be passed to the script. + * @return The return value of the script or {@literal null} if {@link RedisScript#getResultType()} is + * {@literal null}, likely indicating a throw-away status reply (i.e. "OK") */ T execute(RedisScript script, List keys, Object... args); @@ -42,13 +42,13 @@ public interface ScriptExecutor { * Executes the given {@link RedisScript}, using the provided {@link RedisSerializer}s to serialize the script * arguments and result. * - * @param script The script to execute - * @param argsSerializer The {@link RedisSerializer} to use for serializing args - * @param resultSerializer The {@link RedisSerializer} to use for serializing the script return value - * @param keys Any keys that need to be passed to the script - * @param args Any args that need to be passed to the script - * @return The return value of the script or null if {@link RedisScript#getResultType()} is null, likely indicating a - * throw-away status reply (i.e. "OK") + * @param script the script to execute. + * @param argsSerializer The {@link RedisSerializer} to use for serializing args. + * @param resultSerializer The {@link RedisSerializer} to use for serializing the script return value. + * @param keys any keys that need to be passed to the script. + * @param args any args that need to be passed to the script. + * @return The return value of the script or {@literal null} if {@link RedisScript#getResultType()} is + * {@literal null}, likely indicating a throw-away status reply (i.e. "OK") */ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSerializer resultSerializer, List keys, Object... args); diff --git a/src/main/java/org/springframework/data/redis/core/script/ScriptUtils.java b/src/main/java/org/springframework/data/redis/core/script/ScriptUtils.java index e55dd813b6..24df5ae572 100644 --- a/src/main/java/org/springframework/data/redis/core/script/ScriptUtils.java +++ b/src/main/java/org/springframework/data/redis/core/script/ScriptUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/script/ScriptingException.java b/src/main/java/org/springframework/data/redis/core/script/ScriptingException.java index 09315fb6a7..c0794c828e 100644 --- a/src/main/java/org/springframework/data/redis/core/script/ScriptingException.java +++ b/src/main/java/org/springframework/data/redis/core/script/ScriptingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/types/Expiration.java b/src/main/java/org/springframework/data/redis/core/types/Expiration.java index 81930db228..2e4627dbc5 100644 --- a/src/main/java/org/springframework/data/redis/core/types/Expiration.java +++ b/src/main/java/org/springframework/data/redis/core/types/Expiration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,9 @@ package org.springframework.data.redis.core.types; import java.time.Duration; -import java.util.Objects; import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.core.TimeoutUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -105,8 +105,8 @@ public static Expiration from(Duration duration) { Assert.notNull(duration, "Duration must not be null"); return duration.isZero() ? Expiration.persistent() - : duration.toMillis() % 1000 == 0 ? new Expiration(duration.getSeconds(), TimeUnit.SECONDS) - : new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS); + : TimeoutUtils.hasMillis(duration) ? new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS) + : new Expiration(duration.getSeconds(), TimeUnit.SECONDS); } /** diff --git a/src/main/java/org/springframework/data/redis/core/types/Expirations.java b/src/main/java/org/springframework/data/redis/core/types/Expirations.java new file mode 100644 index 0000000000..7101cb60fa --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/types/Expirations.java @@ -0,0 +1,333 @@ +/* + * Copyright 2025 the original author 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.redis.core.types; + +import java.time.Duration; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Value Object linking a number of keys to their {@link TimeToLive} retaining the order of the original source. + * Dedicated higher level methods interpret raw expiration values retrieved from a Redis Client. + *

      + *
    1. {@link #persistent()} returns keys that do not have an associated time to live
    2. + *
    3. {@link #missing()} returns keys that do not exist and therefore have no associated time to live
    4. + *
    5. {@link #ttl()} returns the ordered list of {@link TimeToLive expirations} based on the raw values
    6. + *
    7. {@link #expiring()} returns the expiring keys along with their {@link Duration time to live}
    8. + *
    + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.5 + */ +public class Expirations { + + private final TimeUnit unit; + private final Map expirations; + + Expirations(TimeUnit unit, Map expirations) { + this.unit = unit; + this.expirations = expirations; + } + + /** + * Factory Method to create {@link Expirations} from raw sources provided in a given {@link TimeUnit}. + * + * @param targetUnit the actual time unit of the raw timeToLive values. + * @param keys the keys to associated with the raw values in timeToLive. Defines the actual order of entries within + * {@link Expirations}. + * @param timeouts the raw Redis time to live values. + * @return new instance of {@link Expirations}. + * @param the key type used + */ + public static Expirations of(TimeUnit targetUnit, List keys, Timeouts timeouts) { + + if (keys.size() != timeouts.size()) { + throw new IllegalArgumentException( + "Keys and Timeouts must be of same size but was %s vs %s".formatted(keys.size(), timeouts.size())); + } + if (keys.size() == 1) { + return new Expirations<>(targetUnit, + Map.of(keys.iterator().next(), TimeToLive.of(timeouts.raw().iterator().next(), timeouts.timeUnit()))); + } + + Map target = CollectionUtils.newLinkedHashMap(keys.size()); + for (int i = 0; i < keys.size(); i++) { + target.put(keys.get(i), TimeToLive.of(timeouts.get(i), timeouts.timeUnit())); + } + return new Expirations<>(targetUnit, target); + } + + /** + * @return an ordered set of keys that do not have a time to live. + */ + public Set persistent() { + return filterByState(TimeToLive.PERSISTENT); + } + + /** + * @return an ordered set of keys that do not exist and therefore do not have a time to live. + */ + public Set missing() { + return filterByState(TimeToLive.MISSING); + } + + /** + * @return an ordered set of all {@link Expirations expirations} where the {@link TimeToLive#value()} is using the + * {@link TimeUnit} defined in {@link #timeUnit()}. + */ + public List ttl() { + return expirations.values().stream().map(it -> it.convert(this.unit)).toList(); + } + + /** + * @return the {@link TimeUnit} for {@link TimeToLive expirations} held by this instance. + */ + public TimeUnit timeUnit() { + return unit; + } + + /** + * @return an ordered {@link List} of {@link java.util.Map.Entry entries} combining keys with their actual time to + * live. {@link TimeToLive#isMissing() Missing} and {@link TimeToLive#isPersistent() persistent} entries are + * skipped. + */ + public List> expiring() { + + return expirations.entrySet().stream().filter(it -> !it.getValue().isMissing() && !it.getValue().isPersistent()) + .map(it -> Map.entry(it.getKey(), toDuration(it.getValue()))).toList(); + } + + /** + * @return the ordered collection of keys that are associated with an expiration. + */ + public Collection keys() { + return expirations.keySet(); + } + + /** + * @param key + * @return the {@link Expirations expirations} where the {@link TimeToLive#value()} is using the {@link TimeUnit} + * defined in {@link #timeUnit()} or {@literal null} if no entry could be found. + */ + @Nullable + public TimeToLive expirationOf(K key) { + + TimeToLive timeToLive = expirations.get(key); + + if (timeToLive == null) { + return null; + } + + return timeToLive.convert(this.unit); + } + + /** + * @param key + * @return the time to live value of the requested key if it exists and the expiration is neither + * {@link TimeToLive#isMissing() missing} nor {@link TimeToLive#isPersistent() persistent}, {@literal null} + * otherwise. + */ + @Nullable + public Duration ttlOf(K key) { + return toDuration(expirationOf(key)); + } + + private Set filterByState(TimeToLive filter) { + return expirations.entrySet().stream().filter(entry -> entry.getValue().equals(filter)).map(Map.Entry::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Nullable + static Duration toDuration(@Nullable TimeToLive timeToLive) { + + if (timeToLive == null || timeToLive.sourceUnit == null) { + return null; + } + + return Duration.of(timeToLive.raw(), timeToLive.sourceUnit.toChronoUnit()); + } + + /** + * Collection of timeouts associated with a {@link TimeUnit}. + * + * @param timeUnit + * @param raw + */ + public record Timeouts(TimeUnit timeUnit, List raw) { + + Long get(int index) { + return raw.get(index); + } + + public int size() { + return raw.size(); + } + + } + + /** + * Expiration holds time to live {@link #raw()} values as returned by a Redis Client. {@link #value()} serves the + * actual timeout in the given temporal context converting the {@link #raw()} value into a target {@link TimeUnit}. + * Dedicated methods such as {@link #isPersistent()} allow interpretation of the raw result. {@link #MISSING} and + * {@link #PERSISTENT} mark predefined states returned by Redis indicating a time to live value could not be retrieved + * due to various reasons. + */ + public static class TimeToLive { + + /** + * Predefined {@link TimeToLive} for a key that does not exist and therefore does not have a time to live. + */ + public static TimeToLive MISSING = new TimeToLive(-2L); + + /** + * Predefined {@link TimeToLive} for a key that exists but does not expire. + */ + public static TimeToLive PERSISTENT = new TimeToLive(-1L); + + private final @Nullable TimeUnit sourceUnit; + private final @Nullable TimeUnit targetUnit; + private final long raw; + + TimeToLive(long value) { + this(value, null); + } + + TimeToLive(long value, @Nullable TimeUnit sourceUnit) { + this(value, sourceUnit, null); + } + + TimeToLive(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) { + this.raw = value; + this.sourceUnit = sourceUnit; + this.targetUnit = targetUnit; + } + + /** + * Factory method for creating {@link TimeToLive} instances, returning predefined ones if the value matches a known + * reserved state. + * + * @param value the TTL value. + * @param timeUnit time unit for the given value. + * @return the {@link TimeToLive} for the given raw value. + */ + public static TimeToLive of(Number value, TimeUnit timeUnit) { + + return switch (value.intValue()) { + case -2 -> MISSING; + case -1 -> PERSISTENT; + default -> new TimeToLive(value.longValue(), timeUnit); + }; + } + + /** + * The raw source value as returned by the Redis Client. + * + * @return the raw data. + */ + public long raw() { + return raw; + } + + /** + * @return the {@link #raw()} value converted into the {@link #convert(TimeUnit) requested} target {@link TimeUnit}. + */ + public long value() { + + if (sourceUnit == null || targetUnit == null) { + return raw; + } + + return targetUnit.convert(raw, sourceUnit); + } + + /** + * @param timeUnit must not be {@literal null}. + * @return the {@link TimeToLive} instance with new target {@link TimeUnit} set for obtaining the {@link #value() + * value}, or the same instance raw value cannot or must not be converted. + */ + public TimeToLive convert(TimeUnit timeUnit) { + + if (sourceUnit == null || ObjectUtils.nullSafeEquals(sourceUnit, timeUnit)) { + return this; + } + + return new TimeToLive(raw, sourceUnit, timeUnit); + } + + /** + * @return {@literal true} if key exists but does not expire. + */ + public boolean isPersistent() { + return PERSISTENT.raw() == raw(); + } + + /** + * @return {@literal true} if key does not exist and therefore does not have a time to live. + */ + public boolean isMissing() { + return MISSING.raw() == raw(); + } + + @Override + public boolean equals(Object o) { + + if (o == this) { + return true; + } + + if (!(o instanceof Expirations.TimeToLive that)) { + return false; + } + + if (!ObjectUtils.nullSafeEquals(this.sourceUnit, that.sourceUnit)) { + return false; + } + + if (!ObjectUtils.nullSafeEquals(this.targetUnit, that.targetUnit)) { + return false; + } + + return this.raw == that.raw; + } + + @Override + public int hashCode() { + return Objects.hash(raw); + } + + @Override + public String toString() { + + return switch ((int) raw()) { + case -2 -> "MISSING"; + case -1 -> "PERSISTENT"; + default -> "%d %s".formatted(raw(), sourceUnit); + }; + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/types/RedisClientInfo.java b/src/main/java/org/springframework/data/redis/core/types/RedisClientInfo.java index 898c25b180..e480d995a0 100644 --- a/src/main/java/org/springframework/data/redis/core/types/RedisClientInfo.java +++ b/src/main/java/org/springframework/data/redis/core/types/RedisClientInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -269,13 +269,12 @@ public static class RedisClientInfoBuilder { public static RedisClientInfo fromString(String source) { - Assert.notNull(source, "Cannot read client properties form 'null'"); + Assert.notNull(source, "Cannot read client properties from 'null'"); Properties properties = new Properties(); try { properties.load(new StringReader(source.replace(' ', '\n'))); } catch (IOException ex) { - String message = String.format("Properties could not be loaded from String '%s'", source); - throw new IllegalArgumentException(message, ex); + throw new IllegalArgumentException("Properties could not be loaded from String '%s'".formatted(source), ex); } return new RedisClientInfo(properties); } diff --git a/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java b/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java index 4d00e271d1..77e5aa10ef 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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,10 +15,11 @@ */ package org.springframework.data.redis.domain.geo; +import java.io.Serial; + import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Shape; -import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -31,6 +32,7 @@ */ public class BoundingBox implements Shape { + @Serial private static final long serialVersionUID = 5215611530535947924L; private final Distance width; @@ -94,10 +96,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof BoundingBox)) { + if (!(o instanceof BoundingBox that)) { return false; } - BoundingBox that = (BoundingBox) o; if (!ObjectUtils.nullSafeEquals(width, that.width)) { return false; } @@ -106,6 +107,6 @@ public boolean equals(@Nullable Object o) { @Override public String toString() { - return String.format("Bounding box: [width=%s, height=%s]", width, height); + return "Bounding box: [width=%s, height=%s]".formatted(width, height); } } diff --git a/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java b/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java index 00f410fd37..7802726248 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/domain/geo/GeoLocation.java b/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java index bd570afab6..3ed71bdbc5 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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. @@ -16,7 +16,6 @@ package org.springframework.data.redis.domain.geo; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -49,12 +48,10 @@ public boolean equals(@Nullable Object o) { return true; } - if (!(o instanceof GeoLocation)) { + if (!(o instanceof GeoLocation that)) { return false; } - GeoLocation that = (GeoLocation) o; - if (!ObjectUtils.nullSafeEquals(name, that.name)) { return false; } diff --git a/src/main/java/org/springframework/data/redis/domain/geo/GeoReference.java b/src/main/java/org/springframework/data/redis/domain/geo/GeoReference.java index b3d728e0b6..75fe501422 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/GeoReference.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/GeoReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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. @@ -133,10 +133,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof GeoReference.GeoMemberReference)) { + if (!(o instanceof GeoMemberReference that)) { return false; } - GeoMemberReference that = (GeoMemberReference) o; return ObjectUtils.nullSafeEquals(member, that.member); } @@ -178,10 +177,9 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof GeoReference.GeoCoordinateReference)) { + if (!(o instanceof GeoCoordinateReference that)) { return false; } - GeoCoordinateReference that = (GeoCoordinateReference) o; if (longitude != that.longitude) { return false; } diff --git a/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java b/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java index c3ddf47818..e324ed41b7 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/domain/geo/Metrics.java b/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java index f79b5983aa..8c8064a497 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/domain/geo/RadiusShape.java b/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java index d8b8213e5a..24d8d61b9a 100644 --- a/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java +++ b/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/redis/hash/BeanUtilsHashMapper.java b/src/main/java/org/springframework/data/redis/hash/BeanUtilsHashMapper.java index ad85827105..98887bfdcb 100644 --- a/src/main/java/org/springframework/data/redis/hash/BeanUtilsHashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/BeanUtilsHashMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -71,7 +71,7 @@ public Map toHash(T object) { return result; } catch (Exception ex) { - throw new IllegalArgumentException(String.format("Cannot describe object %s", object), ex); + throw new IllegalArgumentException("Cannot describe object %s".formatted(object), ex); } } } diff --git a/src/main/java/org/springframework/data/redis/hash/DecoratingStringHashMapper.java b/src/main/java/org/springframework/data/redis/hash/DecoratingStringHashMapper.java index ebbeecfa48..33984c0e01 100644 --- a/src/main/java/org/springframework/data/redis/hash/DecoratingStringHashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/DecoratingStringHashMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/hash/HashMapper.java b/src/main/java/org/springframework/data/redis/hash/HashMapper.java index d7c8e18b72..0ec97f1e89 100644 --- a/src/main/java/org/springframework/data/redis/hash/HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/HashMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/hash/Jackson2HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java index e33243b6b0..8788c41ba4 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,23 +15,12 @@ */ package org.springframework.data.redis.hash; -import static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.EVERYTHING; +import static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.*; import java.io.IOException; import java.text.ParseException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import org.springframework.data.mapping.MappingException; import org.springframework.data.redis.support.collections.CollectionUtils; @@ -76,7 +65,6 @@ * Flattening requires all property names to not interfere with JSON paths. Using dots or brackets in map keys or as * property names is not supported using flattening. The resulting hash cannot be mapped back into an Object. *

    Example

    - *

    *

      * class Person {
      * 	String firstname;
    @@ -188,7 +176,7 @@ public boolean useForType(JavaType type) {
     							return false;
     						}
     
    -						if (flatten && type.isTypeOrSubTypeOf(Number.class)) {
    +						if (flatten && (type.isTypeOrSubTypeOf(Number.class) || type.isEnumType())) {
     							return false;
     						}
     
    @@ -379,13 +367,11 @@ private void doFlatten(String propertyPrefix, Iterator>
     
     	private void flattenElement(String propertyPrefix, Object source, Map resultMap) {
     
    -		if (!(source instanceof JsonNode)) {
    +		if (!(source instanceof JsonNode element)) {
     			resultMap.put(propertyPrefix, source);
     			return;
     		}
     
    -		JsonNode element = (JsonNode) source;
    -
     		if (element.isArray()) {
     
     			Iterator nodes = element.elements();
    @@ -425,8 +411,7 @@ private void flattenElement(String propertyPrefix, Object source, Map doDestroy() {
     	 */
     	public Collection getActiveSubscriptions() {
     
    -		return subscriptions.entrySet().stream().filter(entry -> entry.getValue().hasRegistration())
    -				.map(Map.Entry::getKey).collect(Collectors.toList());
    +		return subscriptions.entrySet().stream().filter(entry -> entry.getValue().hasRegistration()).map(Map.Entry::getKey)
    +				.collect(Collectors.toList());
     	}
     
     	/**
    @@ -295,8 +295,7 @@ public  Flux> receive(Iterable topics, Seri
     		}
     
     		return doReceive(channelSerializer, messageSerializer,
    -				getRequiredConnection().pubSubCommands().createSubscription(subscriptionListener), patterns,
    -				channels);
    +				getRequiredConnection().pubSubCommands().createSubscription(subscriptionListener), patterns, channels);
     	}
     
     	private  Flux> doReceive(SerializationPair channelSerializer,
    @@ -361,7 +360,7 @@ public  Mono>> receiveLater(Iterable t
     
     			return doReceiveLater(channelSerializer, messageSerializer,
     					getRequiredConnection().pubSubCommands().createSubscription(readyListener), patterns, channels)
    -							.delayUntil(it -> readyListener.getTrigger());
    +					.delayUntil(it -> readyListener.getTrigger());
     		});
     	}
     
    @@ -441,7 +440,7 @@ private ByteBuffer[] getTargets(Iterable topics, Class class
     	private  Message readMessage(RedisElementReader channelSerializer,
     			RedisElementReader messageSerializer, Message message) {
     
    -		if (message instanceof PatternMessage) {
    +		if (message instanceof PatternMessage) {
     
     			PatternMessage patternMessage = (PatternMessage) message;
     
    diff --git a/src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java b/src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java
    index 172a3f825d..a563bdb1ed 100644
    --- a/src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java
    +++ b/src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -101,6 +101,7 @@
      * @author Thomas Darimont
      * @author Mark Paluch
      * @author John Blum
    + * @author Seongjun Lee
      * @see MessageListener
      * @see SubscriptionListener
      */
    @@ -117,7 +118,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
     	public static final long DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME = 2000L;
     
     	/**
    -	 * Default thread name prefix: "RedisListeningContainer-".
    +	 * Default thread name prefix: "RedisMessageListenerContainer-".
     	 */
     	public static final String DEFAULT_THREAD_NAME_PREFIX = ClassUtils.getShortName(RedisMessageListenerContainer.class)
     			+ "-";
    @@ -554,8 +555,8 @@ public final boolean isActive() {
     	 * Adds a message listener to the (potentially running) container. If the container is running, the listener starts
     	 * receiving (matching) messages as soon as possible.
     	 *
    -	 * @param listener message listener
    -	 * @param topics message listener topic
    +	 * @param listener message listener.
    +	 * @param topics message listener topic.
     	 */
     	public void addMessageListener(MessageListener listener, Collection topics) {
     		addListener(listener, topics);
    @@ -565,8 +566,8 @@ public void addMessageListener(MessageListener listener, Collection topics) {
     		removeListener(listener, topics);
    @@ -593,8 +594,8 @@ public void removeMessageListener(@Nullable MessageListener listener, Collection
     	 * Note that this method obeys the Redis (p)unsubscribe semantics - meaning an empty/null collection will remove
     	 * listener from all channels.
     	 *
    -	 * @param listener message listener
    -	 * @param topic message topic
    +	 * @param listener message listener.
    +	 * @param topic message topic.
     	 */
     	public void removeMessageListener(@Nullable MessageListener listener, Topic topic) {
     		removeMessageListener(listener, Collections.singleton(topic));
    @@ -604,7 +605,7 @@ public void removeMessageListener(@Nullable MessageListener listener, Topic topi
     	 * Removes the given message listener completely (from all topics). If the container is running, the listener stops
     	 * receiving (matching) messages as soon as possible.
     	 *
    -	 * @param listener message listener
    +	 * @param listener message listener.
     	 */
     	public void removeMessageListener(MessageListener listener) {
     
    @@ -657,14 +658,14 @@ private void addListener(MessageListener listener, Collection t
     				Collection collection = resolveMessageListeners(this.channelMapping, serializedTopic);
     				collection.add(listener);
     				channels.add(serializedTopic.getArray());
    -				logTrace(() -> String.format("Adding listener '%s' on channel '%s'", listener, topic.getTopic()));
    +				logTrace(() -> "Adding listener '%s' on channel '%s'".formatted(listener, topic.getTopic()));
     			} else if (topic instanceof PatternTopic) {
     				Collection collection = resolveMessageListeners(this.patternMapping, serializedTopic);
     				collection.add(listener);
     				patterns.add(serializedTopic.getArray());
    -				logTrace(() -> String.format("Adding listener '%s' for pattern '%s'", listener, topic.getTopic()));
    +				logTrace(() -> "Adding listener '%s' for pattern '%s'".formatted(listener, topic.getTopic()));
     			} else {
    -				throw new IllegalArgumentException(String.format("Unknown topic type '%s'", topic.getClass()));
    +				throw new IllegalArgumentException("Unknown topic type '%s'".formatted(topic.getClass()));
     			}
     		}
     		boolean wasListening = isListening();
    @@ -748,12 +749,12 @@ private void removeListener(@Nullable MessageListener listener, Collection String.format("Removing listener '%s' from channel '%s'", listener, topic.getTopic()));
    +				logTrace(() -> "Removing listener '%s' from channel '%s'".formatted(listener, topic.getTopic()));
     			}
     
     			else if (topic instanceof PatternTopic) {
     				remove(listener, topic, holder, patternMapping, patternsToRemove);
    -				logTrace(() -> String.format("Removing listener '%s' from pattern '%s'", listener, topic.getTopic()));
    +				logTrace(() -> "Removing listener '%s' from pattern '%s'".formatted(listener, topic.getTopic()));
     			}
     		}
     
    @@ -770,33 +771,35 @@ else if (isListening()) {
     	}
     
     	private void remove(@Nullable MessageListener listener, Topic topic, ByteArrayWrapper holder,
    -			Map> mapping, List topicToRemove) {
    +						Map> mapping, List topicToRemove) {
     
     		Collection listeners = mapping.get(holder);
    -		Collection listenersToRemove = null;
    -
    -		if (listeners != null) {
    -			// remove only one listener
    -			listeners.remove(listener);
    -			listenersToRemove = Collections.singletonList(listener);
    -
    -			// start removing listeners
    -			for (MessageListener messageListener : listenersToRemove) {
    -				Set topics = listenerTopics.get(messageListener);
    -				if (topics != null) {
    -					topics.remove(topic);
    -				}
    -				if (CollectionUtils.isEmpty(topics)) {
    -					listenerTopics.remove(messageListener);
    -				}
    -			}
    +		if (CollectionUtils.isEmpty(listeners)) {
    +			return;
    +		}
     
    -			// if we removed everything, remove the empty holder collection
    -			if (listeners.isEmpty()) {
    -				mapping.remove(holder);
    -				topicToRemove.add(holder.getArray());
    +		Collection listenersToRemove = (listener == null) ? new ArrayList<>(listeners)
    +				: Collections.singletonList(listener);
    +
    +		// Remove the specified listener(s) from the original collection
    +		listeners.removeAll(listenersToRemove);
    +
    +		// Start removing listeners
    +		for (MessageListener messageListener : listenersToRemove) {
    +			Set topics = listenerTopics.get(messageListener);
    +			if (topics != null) {
    +				topics.remove(topic);
    +			}
    +			if (CollectionUtils.isEmpty(topics)) {
    +				listenerTopics.remove(messageListener);
     			}
     		}
    +
    +		// If all listeners were removed, clean up the mapping and the holder
    +		if (listeners.isEmpty()) {
    +			mapping.remove(holder);
    +			topicToRemove.add(holder.getArray());
    +		}
     	}
     
     	private Subscriber createSubscriber(RedisConnectionFactory connectionFactory, Executor executor) {
    @@ -874,9 +877,8 @@ protected void handleSubscriptionException(CompletableFuture future, BackO
     				long recoveryInterval = backOffExecution.nextBackOff();
     
     				if (recoveryInterval != BackOffExecution.STOP) {
    -					String message = String.format("Connection failure occurred: %s; Restarting subscription task after %s ms",
    -							cause, recoveryInterval);
    -					logger.error(message, cause);
    +					logger.error("Connection failure occurred: %s; Restarting subscription task after %s ms".formatted(cause,
    +							recoveryInterval), cause);
     				}
     
     				return recoveryInterval;
    diff --git a/src/main/java/org/springframework/data/redis/listener/SynchronizingMessageListener.java b/src/main/java/org/springframework/data/redis/listener/SynchronizingMessageListener.java
    index 14210d859c..050e425f3d 100644
    --- a/src/main/java/org/springframework/data/redis/listener/SynchronizingMessageListener.java
    +++ b/src/main/java/org/springframework/data/redis/listener/SynchronizingMessageListener.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2022-2024 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/redis/listener/Topic.java b/src/main/java/org/springframework/data/redis/listener/Topic.java
    index 23f9b6f2d3..bec4cd3c32 100644
    --- a/src/main/java/org/springframework/data/redis/listener/Topic.java
    +++ b/src/main/java/org/springframework/data/redis/listener/Topic.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -19,13 +19,37 @@
      * Topic for a Redis message. Acts a high-level abstraction on top of Redis low-level channels or patterns.
      *
      * @author Costin Leau
    + * @author Mark Paluch
      */
     public interface Topic {
     
    +	/**
    +	 * Create a new {@link ChannelTopic} for channel subscriptions.
    +	 *
    +	 * @param channelName {@link String name} of the Redis channel; must not be {@literal null}.
    +	 * @return the {@link ChannelTopic} for the given {@code channelName}.
    +	 * @since 3.5
    +	 */
    +	static ChannelTopic channel(String channelName) {
    +		return ChannelTopic.of(channelName);
    +	}
    +
    +	/**
    +	 * Create a new {@link PatternTopic} for channel subscriptions based on a {@code pattern}.
    +	 *
    +	 * @param pattern {@link String pattern} used to match channels; must not be {@literal null} or empty.
    +	 * @return the {@link PatternTopic} for the given {@code pattern}.
    +	 * @since 3.5
    +	 */
    +	static PatternTopic pattern(String pattern) {
    +		return PatternTopic.of(pattern);
    +	}
    +
     	/**
     	 * Returns the topic (as a String).
     	 *
     	 * @return the topic
     	 */
     	String getTopic();
    +
     }
    diff --git a/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java b/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java
    index 2152b4fad9..fb7d827728 100644
    --- a/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java
    +++ b/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -375,13 +375,12 @@ protected void invokeListenerMethod(String methodName, Object[] arguments) {
     			if (targetEx instanceof DataAccessException dataAccessException) {
     				throw dataAccessException;
     			} else {
    -				String message = String.format("Listener method '%s' threw exception", methodName);
    -				throw new RedisListenerExecutionFailedException(message, targetEx);
    +				throw new RedisListenerExecutionFailedException("Listener method '%s' threw exception".formatted(methodName),
    +						targetEx);
     			}
     		} catch (Throwable ex) {
    -			String message = String.format("Failed to invoke target method '%s' with arguments %s", methodName,
    -					ObjectUtils.nullSafeToString(arguments));
    -			throw new RedisListenerExecutionFailedException(message, ex);
    +			throw new RedisListenerExecutionFailedException("Failed to invoke target method '%s' with arguments %s"
    +					.formatted(methodName, ObjectUtils.nullSafeToString(arguments)), ex);
     		}
     	}
     
    diff --git a/src/main/java/org/springframework/data/redis/listener/adapter/RedisListenerExecutionFailedException.java b/src/main/java/org/springframework/data/redis/listener/adapter/RedisListenerExecutionFailedException.java
    index 8a07b95c36..ff22789d16 100644
    --- a/src/main/java/org/springframework/data/redis/listener/adapter/RedisListenerExecutionFailedException.java
    +++ b/src/main/java/org/springframework/data/redis/listener/adapter/RedisListenerExecutionFailedException.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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/redis/repository/cdi/CdiBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java
    index bdb9248f6f..275ed2fd08 100644
    --- a/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java
    +++ b/src/main/java/org/springframework/data/redis/repository/cdi/CdiBean.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -149,8 +149,7 @@ public final void initialize() {
     	public void destroy(T instance, CreationalContext creationalContext) {
     
     		if (log.isDebugEnabled()) {
    -			log.debug(String.format("Destroying bean instance %s for repository type '%s'.", instance.toString(),
    -					beanClass.getName()));
    +			log.debug("Destroying bean instance %s for repository type '%s'".formatted(instance, beanClass.getName()));
     		}
     
     		creationalContext.release();
    @@ -206,7 +205,7 @@ public String getId() {
     
     	@Override
     	public String toString() {
    -		return String.format("CdiBean: type='%s', qualifiers=%s", beanClass.getName(), qualifiers.toString());
    +		return "CdiBean: type='%s', qualifiers=%s".formatted(beanClass.getName(), qualifiers.toString());
     	}
     
     }
    diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java
    index 9a1a8988b8..0220531079 100644
    --- a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java
    +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueAdapterBean.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -70,8 +70,7 @@ private Type getBeanType() {
     				return type;
     			}
     
    -			if (type instanceof ParameterizedType) {
    -				ParameterizedType parameterizedType = (ParameterizedType) type;
    +			if (type instanceof ParameterizedType parameterizedType) {
     				if (parameterizedType.getRawType() instanceof Class
     						&& RedisOperations.class.isAssignableFrom((Class) parameterizedType.getRawType())) {
     					return type;
    diff --git a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java
    index eb6dc61941..e1f80694be 100644
    --- a/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java
    +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisKeyValueTemplateBean.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/cdi/RedisRepositoryBean.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java
    index efd211928a..8a85c7bde4 100644
    --- a/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java
    +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryBean.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/cdi/RedisRepositoryExtension.java b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java
    index c445925f6e..69469551fc 100644
    --- a/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java
    +++ b/src/main/java/org/springframework/data/redis/repository/cdi/RedisRepositoryExtension.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -80,7 +80,7 @@  void processBean(@Observes ProcessBean processBean) {
     
     			if (beanType instanceof Class && RedisKeyValueTemplate.class.isAssignableFrom((Class) beanType)) {
     				if (log.isDebugEnabled()) {
    -					log.debug(String.format("Discovered %s with qualifiers %s.", RedisKeyValueTemplate.class.getName(),
    +					log.debug("Discovered %s with qualifiers %s.".formatted(RedisKeyValueTemplate.class.getName(),
     							bean.getQualifiers()));
     				}
     
    @@ -90,7 +90,7 @@  void processBean(@Observes ProcessBean processBean) {
     
     			if (beanType instanceof Class && RedisKeyValueAdapter.class.isAssignableFrom((Class) beanType)) {
     				if (log.isDebugEnabled()) {
    -					log.debug(String.format("Discovered %s with qualifiers %s.", RedisKeyValueAdapter.class.getName(),
    +					log.debug("Discovered %s with qualifiers %s.".formatted(RedisKeyValueAdapter.class.getName(),
     							bean.getQualifiers()));
     				}
     
    @@ -100,7 +100,8 @@  void processBean(@Observes ProcessBean processBean) {
     
     			if (beanType instanceof Class && RedisOperations.class.isAssignableFrom((Class) beanType)) {
     				if (log.isDebugEnabled()) {
    -					log.debug(String.format("Discovered %s with qualifiers %s.", RedisOperations.class.getName(),
    +					log.debug(
    +							"Discovered %s with qualifiers %s.".formatted(RedisOperations.class.getName(),
     							bean.getQualifiers()));
     				}
     
    @@ -123,7 +124,7 @@ void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanMan
     			CdiRepositoryBean repositoryBean = createRepositoryBean(repositoryType, qualifiers, beanManager);
     
     			if (log.isInfoEnabled()) {
    -				log.info(String.format("Registering bean for %s with qualifiers %s.", repositoryType.getName(), qualifiers));
    +				log.info("Registering bean for %s with qualifiers %s.".formatted(repositoryType.getName(), qualifiers));
     			}
     
     			// Register the bean to the container.
    @@ -148,7 +149,7 @@ private void registerDependenciesIfNecessary(@Observes AfterBeanDiscovery afterB
     
     			if (!redisKeyValueAdapters.containsKey(qualifiers)) {
     				if (log.isInfoEnabled()) {
    -					log.info(String.format("Registering bean for %s with qualifiers %s.", RedisKeyValueAdapter.class.getName(),
    +					log.info("Registering bean for %s with qualifiers %s.".formatted(RedisKeyValueAdapter.class.getName(),
     							qualifiers));
     				}
     				RedisKeyValueAdapterBean redisKeyValueAdapterBean = createRedisKeyValueAdapterBean(qualifiers, beanManager);
    @@ -158,7 +159,7 @@ private void registerDependenciesIfNecessary(@Observes AfterBeanDiscovery afterB
     
     			if (!redisKeyValueTemplates.containsKey(qualifiers)) {
     				if (log.isInfoEnabled()) {
    -					log.info(String.format("Registering bean for %s with qualifiers %s.", RedisKeyValueTemplate.class.getName(),
    +					log.info("Registering bean for %s with qualifiers %s.".formatted(RedisKeyValueTemplate.class.getName(),
     							qualifiers));
     				}
     
    @@ -186,8 +187,8 @@ private  CdiRepositoryBean createRepositoryBean(Class repositoryType, S
     		Bean redisKeyValueTemplate = this.redisKeyValueTemplates.get(qualifiers);
     
     		if (redisKeyValueTemplate == null) {
    -			throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.",
    -					RedisKeyValueTemplate.class.getName(), qualifiers));
    +			throw new UnsatisfiedResolutionException("Unable to resolve a bean for '%s' with qualifiers %s"
    +					.formatted(RedisKeyValueTemplate.class.getName(), qualifiers));
     		}
     
     		// Construct and return the repository bean.
    @@ -208,8 +209,8 @@ private RedisKeyValueAdapterBean createRedisKeyValueAdapterBean(Set
     		Bean> redisOperationsBean = this.redisOperations.get(qualifiers);
     
     		if (redisOperationsBean == null) {
    -			throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.",
    -					RedisOperations.class.getName(), qualifiers));
    +			throw new UnsatisfiedResolutionException("Unable to resolve a bean for '%s' with qualifiers %s."
    +					.formatted(RedisOperations.class.getName(), qualifiers));
     		}
     
     		// Construct and return the repository bean.
    @@ -230,8 +231,8 @@ private RedisKeyValueTemplateBean createRedisKeyValueTemplateBean(Set redisKeyValueAdapterBean = this.redisKeyValueAdapters.get(qualifiers);
     
     		if (redisKeyValueAdapterBean == null) {
    -			throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.",
    -					RedisKeyValueAdapter.class.getName(), qualifiers));
    +			throw new UnsatisfiedResolutionException("Unable to resolve a bean for '%s' with qualifiers %s"
    +					.formatted(RedisKeyValueAdapter.class.getName(), qualifiers));
     		}
     
     		// Construct and return the repository bean.
    diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java
    index eea89db92b..1783584d4e 100644
    --- a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java
    +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author 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,6 +23,7 @@
     import java.lang.annotation.Target;
     
     import org.springframework.beans.factory.FactoryBean;
    +import org.springframework.beans.factory.support.BeanNameGenerator;
     import org.springframework.context.annotation.ComponentScan.Filter;
     import org.springframework.context.annotation.Import;
     import org.springframework.data.keyvalue.core.KeyValueOperations;
    @@ -126,6 +127,13 @@
     	 */
     	Class repositoryBaseClass() default DefaultRepositoryBaseClass.class;
     
    +	/**
    +	 * Configure a specific {@link BeanNameGenerator} to be used when creating the repositoy beans.
    +	 * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate context default.
    +	 * @since 3.4
    +	 */
    +	Class nameGenerator() default BeanNameGenerator.class;
    +
     	/**
     	 * Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected.
     	 *
    diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java
    index 21f255f2e6..a681bfb89f 100644
    --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java
    +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrar.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java
    index 728cab6d19..5d008d5cd1 100644
    --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java
    +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/core/MappingRedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java
    index c05d61f12c..207ab0f5bc 100644
    --- a/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java
    +++ b/src/main/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformation.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author 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,10 +40,9 @@ public MappingRedisEntityInformation(RedisPersistentEntity entity) {
     		super(entity);
     
     		if (!entity.hasIdProperty()) {
    -
     			throw new MappingException(
    -					String.format("Entity %s requires to have an explicit id field; Did you forget to provide one using @Id",
    -							entity.getName()));
    +					("Entity %s requires to have an explicit id field;" + " Did you forget to provide one using @Id")
    +							.formatted(entity.getName()));
     		}
     	}
     }
    diff --git a/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java
    index 7b6a7b1e71..e2568dad64 100644
    --- a/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java
    +++ b/src/main/java/org/springframework/data/redis/repository/core/RedisEntityInformation.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2016-2024 the original author or authors.
    + * Copyright 2016-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/query/ExampleQueryMapper.java b/src/main/java/org/springframework/data/redis/repository/query/ExampleQueryMapper.java
    index fe82d0327e..9a42260958 100644
    --- a/src/main/java/org/springframework/data/redis/repository/query/ExampleQueryMapper.java
    +++ b/src/main/java/org/springframework/data/redis/repository/query/ExampleQueryMapper.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2018-2024 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.
    @@ -139,8 +139,8 @@ private void applyPropertySpec(String path, Predicate hasIndex, ExampleM
     
     		if (!SUPPORTED_MATCHERS.contains(stringMatcher)) {
     			throw new InvalidDataAccessApiUsageException(
    -					String.format("Redis Query-by-Example does not support string matcher %s; Supported matchers are: %s.",
    -							stringMatcher, SUPPORTED_MATCHERS));
    +					("Redis Query-by-Example does not support string matcher %s;" + " Supported matchers are: %s.")
    +							.formatted(stringMatcher, SUPPORTED_MATCHERS));
     		}
     
     		if (exampleSpecAccessor.hasPropertySpecifier(path)) {
    diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java
    index c8eab2d3a4..e6985cd4db 100644
    --- a/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java
    +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisOperationChain.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/query/RedisPartTreeQuery.java b/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java
    index 8ba04ac02e..4b69f8f23d 100644
    --- a/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java
    +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2024 the original author or authors.
    + * Copyright 2024-2025 the original author 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 @@
     import org.springframework.data.repository.query.ParameterAccessor;
     import org.springframework.data.repository.query.ParametersParameterAccessor;
     import org.springframework.data.repository.query.QueryMethod;
    -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
     import org.springframework.data.repository.query.ResultProcessor;
     import org.springframework.data.repository.query.ReturnedType;
    +import org.springframework.data.repository.query.ValueExpressionDelegate;
     import org.springframework.data.repository.query.parser.AbstractQueryCreator;
     import org.springframework.data.util.ReflectionUtils;
     import org.springframework.data.util.Streamable;
    @@ -54,9 +54,9 @@ public class RedisPartTreeQuery extends KeyValuePartTreeQuery {
     
     	private final RedisKeyValueAdapter adapter;
     
    -	public RedisPartTreeQuery(QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider,
    +	public RedisPartTreeQuery(QueryMethod queryMethod, ValueExpressionDelegate valueExpressionDelegate,
     			KeyValueOperations template, Class> queryCreator) {
    -		super(queryMethod, evaluationContextProvider, template, queryCreator);
    +		super(queryMethod, valueExpressionDelegate, template, queryCreator);
     		this.adapter = (RedisKeyValueAdapter) template.getKeyValueAdapter();
     	}
     
    diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java
    index bb5d7dae69..2fbf28d2d0 100644
    --- a/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java
    +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisQueryCreator.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author 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.redis.repository.query;
     
    +import java.util.Collection;
     import java.util.Iterator;
     
     import org.springframework.dao.InvalidDataAccessApiUsageException;
    @@ -29,6 +30,8 @@
     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.lang.Nullable;
    +import org.springframework.util.ClassUtils;
     import org.springframework.util.CollectionUtils;
     
     /**
    @@ -37,6 +40,7 @@
      * @author Christoph Strobl
      * @author Mark Paluch
      * @author John Blum
    + * @author Junghoon Ban
      * @since 1.7
      */
     public class RedisQueryCreator extends AbstractQueryCreator, RedisOperationChain> {
    @@ -57,10 +61,8 @@ private RedisOperationChain from(Part part, Iterator iterator, RedisOper
     			case TRUE -> sink.sismember(part.getProperty().toDotPath(), true);
     			case FALSE -> sink.sismember(part.getProperty().toDotPath(), false);
     			case WITHIN, NEAR -> sink.near(getNearPath(part, iterator));
    -			default -> {
    -				String message = String.format("%s is not supported for Redis query derivation", part.getType());
    -				throw new IllegalArgumentException(message);
    -			}
    +			default ->
    +				throw new IllegalArgumentException("%s is not supported for Redis query derivation".formatted(part.getType()));
     		}
     
     		return sink;
    @@ -78,17 +80,15 @@ protected RedisOperationChain or(RedisOperationChain base, RedisOperationChain c
     	}
     
     	@Override
    -	protected KeyValueQuery complete(final RedisOperationChain criteria, Sort sort) {
    +	protected KeyValueQuery complete(@Nullable RedisOperationChain criteria, Sort sort) {
     
     		KeyValueQuery query = new KeyValueQuery<>(criteria);
     
    -		if (query.getCriteria() != null && !CollectionUtils.isEmpty(query.getCriteria().getSismember())
    -				&& !CollectionUtils.isEmpty(query.getCriteria().getOrSismember()))
    -			if (query.getCriteria().getSismember().size() == 1 && query.getCriteria().getOrSismember().size() == 1) {
    -
    -				query.getCriteria().getOrSismember().add(query.getCriteria().getSismember().iterator().next());
    -				query.getCriteria().getSismember().clear();
    -			}
    +		if (criteria != null && containsExactlyOne(criteria.getSismember())
    +				&& containsExactlyOne(criteria.getOrSismember())) {
    +			criteria.getOrSismember().addAll(criteria.getSismember());
    +			criteria.getSismember().clear();
    +		}
     
     		if (sort.isSorted()) {
     			query.setSort(sort);
    @@ -99,43 +99,42 @@ protected KeyValueQuery complete(final RedisOperationChain
     
     	private NearPath getNearPath(Part part, Iterator iterator) {
     
    +		String path = part.getProperty().toDotPath();
     		Object value = iterator.next();
     
    -		Point point;
    -		Distance distance;
    -
    -		if (value instanceof Circle) {
    -			point = ((Circle) value).getCenter();
    -			distance = ((Circle) value).getRadius();
    -		} else if (value instanceof Point) {
    +		if (value instanceof Circle circle) {
    +			return new NearPath(path, circle.getCenter(), circle.getRadius());
    +		}
     
    -			point = (Point) value;
    +		if (value instanceof Point point) {
     
     			if (!iterator.hasNext()) {
    -				String message = "Expected to find distance value for geo query; Are you missing a parameter";
    -				throw new InvalidDataAccessApiUsageException(message);
    +				throw new InvalidDataAccessApiUsageException(
    +						"Expected to find distance value for geo query;" + " Are you missing a parameter?");
     			}
     
    +			Distance distance;
     			Object distObject = iterator.next();
    -			if (distObject instanceof Distance) {
    -				distance = (Distance) distObject;
    -			} else if (distObject instanceof Number) {
    -				distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS);
    -			} else {
     
    -				String message = String.format("Expected to find Distance or Numeric value for geo query but was %s",
    -						distObject.getClass());
    +			if (distObject instanceof Distance dist) {
    +				distance = dist;
    +			} else if (distObject instanceof Number num) {
    +				distance = new Distance(num.doubleValue(), Metrics.KILOMETERS);
    +			} else {
     
    -				throw new InvalidDataAccessApiUsageException(message);
    +				throw new InvalidDataAccessApiUsageException(
    +						"Expected to find Distance or Numeric value for geo query but was %s"
    +								.formatted(ClassUtils.getDescriptiveType(distObject)));
     			}
    -		} else {
    -
    -			String message = String.format("Expected to find a Circle or Point/Distance for geo query but was %s.",
    -					value.getClass());
     
    -			throw new InvalidDataAccessApiUsageException(message);
    +			return new NearPath(path, point, distance);
     		}
     
    -		return new NearPath(part.getProperty().toDotPath(), point, distance);
    +		throw new InvalidDataAccessApiUsageException("Expected to find a Circle or Point/Distance for geo query but was %s"
    +				.formatted(ClassUtils.getDescriptiveType(value.getClass())));
    +	}
    +
    +	private static boolean containsExactlyOne(Collection collection) {
    +		return !CollectionUtils.isEmpty(collection) && collection.size() == 1;
     	}
     }
    diff --git a/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java b/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java
    index 6a3a9584d2..44704c6096 100644
    --- a/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java
    +++ b/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2018-2024 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/redis/repository/support/RedisRepositoryFactory.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java
    index 5197d7665c..d6787357dc 100644
    --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java
    +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author or authors.
      *
      * Licensed under 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/redis/repository/support/RedisRepositoryFactoryBean.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java
    index b0ecb53970..37cf0896c5 100644
    --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java
    +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author or authors.
      *
      * Licensed under 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/redis/serializer/ByteArrayRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/ByteArrayRedisSerializer.java
    index 57a2cb9e57..cb24add7a0 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/ByteArrayRedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/ByteArrayRedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2018-2024 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/redis/serializer/DefaultRedisElementReader.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java
    index 0be8d02331..be012f84d0 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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/redis/serializer/DefaultRedisElementWriter.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java
    index 43fa91fa2d..e6f75c5f70 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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.
    @@ -18,6 +18,7 @@
     import java.nio.ByteBuffer;
     
     import org.springframework.lang.Nullable;
    +import org.springframework.util.ObjectUtils;
     
     /**
      * Default implementation of {@link RedisElementWriter}.
    @@ -38,7 +39,8 @@ class DefaultRedisElementWriter implements RedisElementWriter {
     	public ByteBuffer write(@Nullable T value) {
     
     		if (serializer != null && (value == null || serializer.canSerialize(value.getClass()))) {
    -			return ByteBuffer.wrap(serializer.serialize(value));
    +			byte[] serializedValue = serializer.serialize(value);
    +			return serializedValue != null ? ByteBuffer.wrap(serializedValue) : ByteBuffer.wrap(new byte[0]);
     		}
     
     		if (value instanceof byte[]) {
    @@ -50,6 +52,6 @@ public ByteBuffer write(@Nullable T value) {
     		}
     
     		throw new IllegalStateException(
    -				String.format("Cannot serialize value of type %s without a serializer", value.getClass()));
    +				"Cannot serialize value of type %s without a serializer".formatted(ObjectUtils.nullSafeClassName(value)));
     	}
     }
    diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisSerializationContext.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisSerializationContext.java
    index 8232b17db8..b302ea64b9 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisSerializationContext.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisSerializationContext.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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/redis/serializer/DefaultSerializationPair.java b/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationPair.java
    index 2e4527d503..9f5f9491ef 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationPair.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationPair.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
    index 28b9474b26..c2973aa627 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2015-2024 the original author or authors.
    + * Copyright 2015-2025 the original author 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,13 +32,19 @@
     import com.fasterxml.jackson.annotation.JsonTypeInfo;
     import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
     import com.fasterxml.jackson.core.JsonGenerator;
    +import com.fasterxml.jackson.core.JsonParser;
    +import com.fasterxml.jackson.core.JsonToken;
     import com.fasterxml.jackson.core.TreeNode;
     import com.fasterxml.jackson.databind.DeserializationConfig;
     import com.fasterxml.jackson.databind.JavaType;
    +import com.fasterxml.jackson.databind.JsonDeserializer;
     import com.fasterxml.jackson.databind.JsonNode;
     import com.fasterxml.jackson.databind.ObjectMapper;
     import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
     import com.fasterxml.jackson.databind.SerializerProvider;
    +import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory;
    +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext;
    +import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
     import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
     import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
     import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
    @@ -60,6 +66,7 @@
      * @author Mark Paluch
      * @author Mao Shuai
      * @author John Blum
    + * @author Anne Lee
      * @see org.springframework.data.redis.serializer.JacksonObjectReader
      * @see org.springframework.data.redis.serializer.JacksonObjectWriter
      * @see com.fasterxml.jackson.databind.ObjectMapper
    @@ -92,13 +99,13 @@ public GenericJackson2JsonRedisSerializer() {
     	 * In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS} will be
     	 * used.
     	 *
    -	 * @param classPropertyTypeName {@link String name} of the JSON property holding type information; can be
    +	 * @param typeHintPropertyName {@link String name} of the JSON property holding type information; can be
     	 *          {@literal null}.
     	 * @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
     	 * @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
     	 */
    -	public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
    -		this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create());
    +	public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName) {
    +		this(typeHintPropertyName, JacksonObjectReader.create(), JacksonObjectWriter.create());
     	}
     
     	/**
    @@ -109,7 +116,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
     	 * In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS} will be
     	 * used.
     	 *
    -	 * @param classPropertyTypeName {@link String name} of the JSON property holding type information; can be
    +	 * @param typeHintPropertyName {@link String name} of the JSON property holding type information; can be
     	 *          {@literal null}.
     	 * @param reader {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
     	 * @param writer {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
    @@ -117,21 +124,14 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
     	 * @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
     	 * @since 3.0
     	 */
    -	public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName, JacksonObjectReader reader,
    +	public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, JacksonObjectReader reader,
     			JacksonObjectWriter writer) {
     
    -		this(new ObjectMapper(), reader, writer, classPropertyTypeName);
    +		this(new ObjectMapper(), reader, writer, typeHintPropertyName);
     
    -		registerNullValueSerializer(this.mapper, classPropertyTypeName);
    +		registerNullValueSerializer(this.mapper, typeHintPropertyName);
     
    -		StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(this.mapper).init(JsonTypeInfo.Id.CLASS, null)
    -				.inclusion(JsonTypeInfo.As.PROPERTY);
    -
    -		if (StringUtils.hasText(classPropertyTypeName)) {
    -			typer = typer.typeProperty(classPropertyTypeName);
    -		}
    -
    -		this.mapper.setDefaultTyping(typer);
    +		this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(getObjectMapper(), typeHintPropertyName));
     	}
     
     	/**
    @@ -157,7 +157,6 @@ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
     	 */
     	public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader,
     			JacksonObjectWriter writer) {
    -
     		this(mapper, reader, writer, null);
     	}
     
    @@ -177,7 +176,7 @@ private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectRea
     		this.typeResolver = newTypeResolver(mapper, typeHintPropertyName, this.defaultTypingEnabled);
     	}
     
    -	private TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeHintPropertyName,
    +	private static TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeHintPropertyName,
     			Lazy defaultTypingEnabled) {
     
     		Lazy lazyTypeFactory = Lazy.of(mapper::getTypeFactory);
    @@ -185,22 +184,20 @@ private TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeH
     		Lazy lazyTypeHintPropertyName = typeHintPropertyName != null ? Lazy.of(typeHintPropertyName)
     				: newLazyTypeHintPropertyName(mapper, defaultTypingEnabled);
     
    -		return new TypeResolver(lazyTypeFactory, lazyTypeHintPropertyName);
    +		return new TypeResolver(mapper, lazyTypeFactory, lazyTypeHintPropertyName);
     	}
     
    -	private Lazy newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy defaultTypingEnabled) {
    +	private static Lazy newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy defaultTypingEnabled) {
     
     		Lazy configuredTypeDeserializationPropertyName = getConfiguredTypeDeserializationPropertyName(mapper);
     
    -		Lazy resolvedLazyTypeHintPropertyName = Lazy.of(() -> defaultTypingEnabled.get() ? null
    -				: configuredTypeDeserializationPropertyName.get());
    +		Lazy resolvedLazyTypeHintPropertyName = Lazy
    +				.of(() -> defaultTypingEnabled.get() ? null : configuredTypeDeserializationPropertyName.get());
     
    -		resolvedLazyTypeHintPropertyName = resolvedLazyTypeHintPropertyName.or("@class");
    -
    -		return resolvedLazyTypeHintPropertyName;
    +		return resolvedLazyTypeHintPropertyName.or("@class");
     	}
     
    -	private Lazy getConfiguredTypeDeserializationPropertyName(ObjectMapper mapper) {
    +	private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectMapper mapper) {
     
     		return Lazy.of(() -> {
     
    @@ -215,20 +212,43 @@ private Lazy getConfiguredTypeDeserializationPropertyName(ObjectMapper m
     		});
     	}
     
    +	private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(ObjectMapper objectMapper,
    +			@Nullable String typeHintPropertyName) {
    +
    +		StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(objectMapper).init(JsonTypeInfo.Id.CLASS, null)
    +				.inclusion(As.PROPERTY);
    +
    +		if (StringUtils.hasText(typeHintPropertyName)) {
    +			typer = typer.typeProperty(typeHintPropertyName);
    +		}
    +		return typer;
    +	}
    +
    +	/**
    +	 * Factory method returning a {@literal Builder} used to construct and configure a
    +	 * {@link GenericJackson2JsonRedisSerializer}.
    +	 *
    +	 * @return new {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +	 * @since 3.3.1
    +	 */
    +	public static GenericJackson2JsonRedisSerializerBuilder builder() {
    +		return new GenericJackson2JsonRedisSerializerBuilder();
    +	}
    +
     	/**
     	 * Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
    -	 * {@code classPropertyTypeName}. This method should be called by code that customizes
    +	 * {@code typeHintPropertyName}. This method should be called by code that customizes
     	 * {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
     	 *
     	 * @param objectMapper the object mapper to customize.
    -	 * @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
    +	 * @param typeHintPropertyName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
     	 * @since 2.2
     	 */
    -	public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {
    +	public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String typeHintPropertyName) {
     
     		// Simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here
     		// since we need the type hint embedded for deserialization using the default typing feature.
    -		objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
    +		objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(typeHintPropertyName)));
     	}
     
     	/**
    @@ -251,12 +271,12 @@ public byte[] serialize(@Nullable Object value) throws SerializationException {
     		try {
     			return writer.write(mapper, value);
     		} catch (IOException ex) {
    -			String message = String.format("Could not write JSON: %s", ex.getMessage());
    -			throw new SerializationException(message, ex);
    +			throw new SerializationException("Could not write JSON: %s".formatted(ex.getMessage()), ex);
     		}
     	}
     
     	@Override
    +	@Nullable
     	public Object deserialize(@Nullable byte[] source) throws SerializationException {
     		return deserialize(source, Object.class);
     	}
    @@ -288,8 +308,7 @@ public  T deserialize(@Nullable byte[] source, Class type) throws Serializ
     		try {
     			return (T) reader.read(mapper, source, resolveType(source, type));
     		} catch (Exception ex) {
    -			String message = String.format("Could not read JSON:%s ", ex.getMessage());
    -			throw new SerializationException(message, ex);
    +			throw new SerializationException("Could not read JSON:%s ".formatted(ex.getMessage()), ex);
     		}
     	}
     
    @@ -327,14 +346,13 @@ protected JavaType resolveType(byte[] source, Class type) throws IOException
     	 */
     	static class TypeResolver {
     
    -		// need a separate instance to bypass class hint checks
    -		private final ObjectMapper mapper = new ObjectMapper();
    -
    +		private final ObjectMapper mapper;
     		private final Supplier typeFactory;
     		private final Supplier hintName;
     
    -		TypeResolver(Supplier typeFactory, Supplier hintName) {
    +		TypeResolver(ObjectMapper mapper, Supplier typeFactory, Supplier hintName) {
     
    +			this.mapper = mapper;
     			this.typeFactory = typeFactory;
     			this.hintName = hintName;
     		}
    @@ -345,7 +363,7 @@ protected JavaType constructType(Class type) {
     
     		protected JavaType resolveType(byte[] source, Class type) throws IOException {
     
    -			JsonNode root = mapper.readTree(source);
    +			JsonNode root = readTree(source);
     			JsonNode jsonNode = root.get(hintName.get());
     
     			if (jsonNode instanceof TextNode && jsonNode.asText() != null) {
    @@ -354,6 +372,44 @@ protected JavaType resolveType(byte[] source, Class type) throws IOException
     
     			return constructType(type);
     		}
    +
    +		/**
    +		 * Lenient variant of ObjectMapper._readTreeAndClose using a strict {@link JsonNodeDeserializer}.
    +		 */
    +		private JsonNode readTree(byte[] source) throws IOException {
    +
    +			JsonDeserializer deserializer = JsonNodeDeserializer.getDeserializer(JsonNode.class);
    +			DeserializationConfig cfg = mapper.getDeserializationConfig();
    +
    +			try (JsonParser parser = createParser(source, cfg)) {
    +
    +				JsonToken t = parser.currentToken();
    +				if (t == null) {
    +					t = parser.nextToken();
    +					if (t == null) {
    +						return cfg.getNodeFactory().missingNode();
    +					}
    +				}
    +
    +				/*
    +				 * Hokey pokey! Oh my.
    +				 */
    +				DefaultDeserializationContext ctxt = new DefaultDeserializationContext.Impl(BeanDeserializerFactory.instance)
    +						.createInstance(cfg, parser, mapper.getInjectableValues());
    +				if (t == JsonToken.VALUE_NULL) {
    +					return cfg.getNodeFactory().nullNode();
    +				} else {
    +					return deserializer.deserialize(parser, ctxt);
    +				}
    +			}
    +		}
    +
    +		private JsonParser createParser(byte[] source, DeserializationConfig cfg) throws IOException {
    +
    +			JsonParser parser = mapper.createParser(source);
    +			cfg.initialize(parser);
    +			return parser;
    +		}
     	}
     
     	/**
    @@ -365,8 +421,7 @@ protected JavaType resolveType(byte[] source, Class type) throws IOException
     	 */
     	private static class NullValueSerializer extends StdSerializer {
     
    -		@Serial
    -		private static final long serialVersionUID = 1999052150548658808L;
    +		@Serial private static final long serialVersionUID = 1999052150548658808L;
     
     		private final String classIdentifier;
     
    @@ -396,6 +451,159 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri
     		}
     	}
     
    +	/**
    +	 * Builder for configuring and creating a {@link GenericJackson2JsonRedisSerializer}.
    +	 *
    +	 * @author Anne Lee
    +	 * @author Mark Paluch
    +	 * @since 3.3.1
    +	 */
    +	public static class GenericJackson2JsonRedisSerializerBuilder {
    +
    +		private @Nullable String typeHintPropertyName;
    +
    +		private JacksonObjectReader reader = JacksonObjectReader.create();
    +
    +		private JacksonObjectWriter writer = JacksonObjectWriter.create();
    +
    +		private @Nullable ObjectMapper objectMapper;
    +
    +		private @Nullable Boolean defaultTyping;
    +
    +		private boolean registerNullValueSerializer = true;
    +
    +		private @Nullable StdSerializer nullValueSerializer;
    +
    +		private GenericJackson2JsonRedisSerializerBuilder() {}
    +
    +		/**
    +		 * Enable or disable default typing. Enabling default typing will override
    +		 * {@link ObjectMapper#setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder)} for a given
    +		 * {@link ObjectMapper}. Default typing is enabled by default if no {@link ObjectMapper} is provided.
    +		 *
    +		 * @param defaultTyping whether to enable/disable default typing. Enabled by default if the {@link ObjectMapper} is
    +		 *          not provided.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) {
    +			this.defaultTyping = defaultTyping;
    +			return this;
    +		}
    +
    +		/**
    +		 * Configure a property name to that represents the type hint.
    +		 *
    +		 * @param typeHintPropertyName {@link String name} of the JSON property holding type information.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder typeHintPropertyName(String typeHintPropertyName) {
    +
    +			Assert.hasText(typeHintPropertyName, "Type hint property name must bot be null or empty");
    +
    +			this.typeHintPropertyName = typeHintPropertyName;
    +			return this;
    +		}
    +
    +		/**
    +		 * Configure a provided {@link ObjectMapper}. Note that the provided {@link ObjectMapper} can be reconfigured with a
    +		 * {@link #nullValueSerializer} or default typing depending on the builder configuration.
    +		 *
    +		 * @param objectMapper must not be {@literal null}.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder objectMapper(ObjectMapper objectMapper) {
    +
    +			Assert.notNull(objectMapper, "ObjectMapper must not be null");
    +
    +			this.objectMapper = objectMapper;
    +			return this;
    +		}
    +
    +		/**
    +		 * Configure {@link JacksonObjectReader}.
    +		 *
    +		 * @param reader must not be {@literal null}.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder reader(JacksonObjectReader reader) {
    +
    +			Assert.notNull(reader, "JacksonObjectReader must not be null");
    +
    +			this.reader = reader;
    +			return this;
    +		}
    +
    +		/**
    +		 * Configure {@link JacksonObjectWriter}.
    +		 *
    +		 * @param writer must not be {@literal null}.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder writer(JacksonObjectWriter writer) {
    +
    +			Assert.notNull(writer, "JacksonObjectWriter must not be null");
    +
    +			this.writer = writer;
    +			return this;
    +		}
    +
    +		/**
    +		 * Register a {@link StdSerializer serializer} for {@link NullValue}.
    +		 *
    +		 * @param nullValueSerializer the {@link StdSerializer} to use for {@link NullValue} serialization, must not be
    +		 *          {@literal null}.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder nullValueSerializer(StdSerializer nullValueSerializer) {
    +
    +			Assert.notNull(nullValueSerializer, "Null value serializer must not be null");
    +
    +			this.nullValueSerializer = nullValueSerializer;
    +			return this;
    +		}
    +
    +		/**
    +		 * Configure whether to register a {@link StdSerializer serializer} for {@link NullValue} serialization. The default
    +		 * serializer considers {@link #typeHintPropertyName(String)}.
    +		 *
    +		 * @param registerNullValueSerializer {@code true} to register the default serializer; {@code false} otherwise.
    +		 * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
    +		 */
    +		public GenericJackson2JsonRedisSerializerBuilder registerNullValueSerializer(boolean registerNullValueSerializer) {
    +			this.registerNullValueSerializer = registerNullValueSerializer;
    +			return this;
    +		}
    +
    +		/**
    +		 * Creates a new instance of {@link GenericJackson2JsonRedisSerializer} with configuration options applied. Creates
    +		 * also a new {@link ObjectMapper} if none was provided.
    +		 *
    +		 * @return a new instance of {@link GenericJackson2JsonRedisSerializer}.
    +		 */
    +		public GenericJackson2JsonRedisSerializer build() {
    +
    +			ObjectMapper objectMapper = this.objectMapper;
    +			boolean providedObjectMapper = objectMapper != null;
    +
    +			if (objectMapper == null) {
    +				objectMapper = new ObjectMapper();
    +			}
    +
    +			if (registerNullValueSerializer) {
    +				objectMapper.registerModule(new SimpleModule("GenericJackson2JsonRedisSerializerBuilder")
    +						.addSerializer(this.nullValueSerializer != null ? this.nullValueSerializer
    +								: new NullValueSerializer(this.typeHintPropertyName)));
    +			}
    +
    +			if ((!providedObjectMapper && (defaultTyping == null || defaultTyping))
    +					|| (defaultTyping != null && defaultTyping)) {
    +				objectMapper.setDefaultTyping(createDefaultTypeResolverBuilder(objectMapper, typeHintPropertyName));
    +			}
    +
    +			return new GenericJackson2JsonRedisSerializer(objectMapper, this.reader, this.writer, this.typeHintPropertyName);
    +		}
    +	}
    +
     	/**
     	 * Custom {@link StdTypeResolverBuilder} that considers typing for non-primitive types. Primitives, their wrappers and
     	 * primitive arrays do not require type hints. The default {@code DefaultTyping#EVERYTHING} typing does not satisfy
    @@ -424,6 +632,7 @@ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class defaultI
     		 * Boolean, Integer, Double) will never use typing; that is both due to them being concrete and final, and since
     		 * actual serializers and deserializers will also ignore any attempts to enforce typing.
     		 */
    +		@Override
     		public boolean useForType(JavaType javaType) {
     
     			if (javaType.isJavaLangObject()) {
    diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericToStringSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericToStringSerializer.java
    index 1071f6ee52..8159347b89 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/GenericToStringSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/GenericToStringSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -92,6 +92,7 @@ public byte[] serialize(@Nullable T value) {
     	}
     
     	@Override
    +	@Nullable
     	public T deserialize(@Nullable byte[] bytes) {
     
     		if (bytes == null) {
    diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
    index 76238bc60d..32d626bdd8 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -158,6 +158,7 @@ public byte[] serialize(@Nullable T value) throws SerializationException {
     		}
     	}
     
    +	@Nullable
     	@Override
     	@SuppressWarnings("unchecked")
     	public T deserialize(@Nullable byte[] bytes) throws SerializationException {
    diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
    index 4e76cd4ba7..e2c1d943ec 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2022-2024 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/redis/serializer/JacksonObjectWriter.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java
    index b7383697f7..88db313130 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2022-2024 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/redis/serializer/JdkSerializationRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/JdkSerializationRedisSerializer.java
    index 00c79c6f8a..db67928a94 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/JdkSerializationRedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/JdkSerializationRedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -20,8 +20,8 @@
     import org.springframework.core.serializer.DefaultSerializer;
     import org.springframework.core.serializer.support.DeserializingConverter;
     import org.springframework.core.serializer.support.SerializingConverter;
    -import org.springframework.data.redis.util.RedisAssertions;
     import org.springframework.lang.Nullable;
    +import org.springframework.util.Assert;
     
     /**
      * Java Serialization {@link RedisSerializer}.
    @@ -77,10 +77,14 @@ public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) {
     	public JdkSerializationRedisSerializer(Converter serializer,
     			Converter deserializer) {
     
    -		this.serializer = RedisAssertions.requireNonNull(serializer, "Serializer must not be null");
    -		this.deserializer = RedisAssertions.requireNonNull(deserializer, "Deserializer must not be null");
    +		Assert.notNull(serializer, "Serializer must not be null");
    +		Assert.notNull(deserializer, "Deserializer must not be null");
    +
    +		this.serializer = serializer;
    +		this.deserializer = deserializer;
     	}
     
    +	@Nullable
     	@Override
     	public byte[] serialize(@Nullable Object value) {
     
    @@ -95,6 +99,7 @@ public byte[] serialize(@Nullable Object value) {
     		}
     	}
     
    +	@Nullable
     	@Override
     	public Object deserialize(@Nullable byte[] bytes) {
     
    diff --git a/src/main/java/org/springframework/data/redis/serializer/OxmSerializer.java b/src/main/java/org/springframework/data/redis/serializer/OxmSerializer.java
    index fb1eb4da5e..18ceace01f 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/OxmSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/OxmSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -102,6 +102,7 @@ public byte[] serialize(@Nullable Object value) throws SerializationException {
     		return stream.toByteArray();
     	}
     
    +	@Nullable
     	@Override
     	public Object deserialize(@Nullable byte[] bytes) throws SerializationException {
     
    diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java b/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java
    index 445bebf45a..fb2c37dac4 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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/redis/serializer/RedisElementWriter.java b/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java
    index fe30afbe50..c67f2b0fa8 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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/redis/serializer/RedisSerializationContext.java b/src/main/java/org/springframework/data/redis/serializer/RedisSerializationContext.java
    index f3157b3407..9136d05cdc 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/RedisSerializationContext.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/RedisSerializationContext.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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.
    @@ -242,7 +242,7 @@ static  SerializationPair raw() {
     		}
     
     		/**
    -		 * Creates a pass through {@link SerializationPair} to pass-thru {@link byte} objects.
    +		 * Creates a pass through {@link SerializationPair} to pass-thru {@code byte} objects.
     		 *
     		 * @return a pass through {@link SerializationPair}.
     		 * @since 2.2
    diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/RedisSerializer.java
    index 648a69dcd7..709bd4c794 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/RedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/RedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -65,7 +65,7 @@ static RedisSerializer json() {
     	}
     
     	/**
    -	 * Obtain a simple {@link java.lang.String} to {@literal byte[]} (and back) serializer using
    +	 * Obtain a simple {@link java.lang.String} to {@code byte[]} (and back) serializer using
     	 * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8} as the default {@link java.nio.charset.Charset}.
     	 *
     	 * @return never {@literal null}.
    diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisSerializerToSerializationPairAdapter.java b/src/main/java/org/springframework/data/redis/serializer/RedisSerializerToSerializationPairAdapter.java
    index 03ca10377d..f7f5b72fcc 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/RedisSerializerToSerializationPairAdapter.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/RedisSerializerToSerializationPairAdapter.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2017-2024 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.
    @@ -55,7 +55,7 @@ static  SerializationPair raw() {
     	}
     
     	/**
    -	 * @return the {@link RedisSerializerToSerializationPairAdapter} for {@link byte[]}.
    +	 * @return the {@link RedisSerializerToSerializationPairAdapter} for {@code byte[]}.
     	 * @since 2.2
     	 */
     	static SerializationPair byteArray() {
    diff --git a/src/main/java/org/springframework/data/redis/serializer/SerializationException.java b/src/main/java/org/springframework/data/redis/serializer/SerializationException.java
    index 3f03667ff1..1493dd6ef1 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/SerializationException.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/SerializationException.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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/redis/serializer/SerializationUtils.java b/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java
    index bd2de9dffc..ee78179598 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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/redis/serializer/StringRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/StringRedisSerializer.java
    index 8af0002f46..1a0abd14bd 100644
    --- a/src/main/java/org/springframework/data/redis/serializer/StringRedisSerializer.java
    +++ b/src/main/java/org/springframework/data/redis/serializer/StringRedisSerializer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2011-2024 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.
    @@ -22,7 +22,7 @@
     import org.springframework.util.Assert;
     
     /**
    - * Simple {@link java.lang.String} to {@literal byte[]} (and back) serializer. Converts {@link java.lang.String Strings}
    + * Simple {@link java.lang.String} to {@code byte[]} (and back) serializer. Converts {@link java.lang.String Strings}
      * into bytes and vice-versa using the specified charset (by default {@literal UTF-8}).
      * 

    * Useful when the interaction with the Redis happens mainly through Strings. @@ -80,11 +80,13 @@ public StringRedisSerializer(Charset charset) { this.charset = charset; } + @Nullable @Override public byte[] serialize(@Nullable String value) { return (value == null ? null : value.getBytes(charset)); } + @Nullable @Override public String deserialize(@Nullable byte[] bytes) { return (bytes == null ? null : new String(bytes, charset)); diff --git a/src/main/java/org/springframework/data/redis/stream/Cancelable.java b/src/main/java/org/springframework/data/redis/stream/Cancelable.java index b7e3cd6369..c1b89a831b 100644 --- a/src/main/java/org/springframework/data/redis/stream/Cancelable.java +++ b/src/main/java/org/springframework/data/redis/stream/Cancelable.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/stream/DefaultStreamMessageListenerContainer.java b/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainer.java index 25cd0d4b13..03c7d6add8 100644 --- a/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainer.java +++ b/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -224,9 +224,7 @@ private Function> getReadFunction(StreamReadRequest byte[] rawKey = ((RedisSerializer) template.getKeySerializer()) .serialize(streamRequest.getStreamOffset().getKey()); - if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) { - - ConsumerStreamReadRequest consumerStreamRequest = (ConsumerStreamReadRequest) streamRequest; + if (streamRequest instanceof ConsumerStreamReadRequest consumerStreamRequest) { StreamReadOptions readOptions = consumerStreamRequest.isAutoAcknowledge() ? this.readOptions.autoAcknowledge() : this.readOptions; diff --git a/src/main/java/org/springframework/data/redis/stream/DefaultStreamReceiver.java b/src/main/java/org/springframework/data/redis/stream/DefaultStreamReceiver.java index 02af99b523..9d9349e18b 100644 --- a/src/main/java/org/springframework/data/redis/stream/DefaultStreamReceiver.java +++ b/src/main/java/org/springframework/data/redis/stream/DefaultStreamReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -105,7 +105,7 @@ class DefaultStreamReceiver> implements StreamReceiver public Flux receive(StreamOffset streamOffset) { if (logger.isDebugEnabled()) { - logger.debug(String.format("receive(%s)", streamOffset)); + logger.debug("receive(%s)".formatted(streamOffset)); } RedisSerializationContext.SerializationPair keySerializer = template.getSerializationContext() @@ -125,11 +125,10 @@ public Flux receive(StreamOffset streamOffset) { } @Override - @SuppressWarnings("unchecked") public Flux receiveAutoAck(Consumer consumer, StreamOffset streamOffset) { if (logger.isDebugEnabled()) { - logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset)); + logger.debug("receiveAutoAck(%s, %s)".formatted(consumer, streamOffset)); } Function> readFunction = getConsumeReadFunction(streamOffset.getKey(), consumer, @@ -146,11 +145,10 @@ public Flux receiveAutoAck(Consumer consumer, StreamOffset streamOffset) { } @Override - @SuppressWarnings("unchecked") public Flux receive(Consumer consumer, StreamOffset streamOffset) { if (logger.isDebugEnabled()) { - logger.debug(String.format("receive(%s, %s)", consumer, streamOffset)); + logger.debug("receive(%s, %s)".formatted(consumer, streamOffset)); } Function> readFunction = getConsumeReadFunction(streamOffset.getKey(), consumer, @@ -229,7 +227,7 @@ void arm() { sink.onRequest(toAdd -> { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onRequest(%d)", key, toAdd)); + logger.debug("[stream: %s] onRequest(%d)".formatted(key, toAdd)); } if (pollState.isSubscriptionActive()) { @@ -251,7 +249,7 @@ void arm() { } } else { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onRequest(%d): Dropping, subscription canceled", key, toAdd)); + logger.debug("[stream: %s] onRequest(%d): Dropping, subscription canceled".formatted(key, toAdd)); } } }); @@ -263,25 +261,25 @@ void arm() { private void scheduleIfRequired() { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] scheduleIfRequired()", key)); + logger.debug("[stream: %s] scheduleIfRequired()".formatted(key)); } if (pollState.isScheduled()) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] scheduleIfRequired(): Already scheduled", key)); + logger.debug("[stream: %s] scheduleIfRequired(): Already scheduled".formatted(key)); } return; } if (!pollState.isSubscriptionActive()) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] scheduleIfRequired(): Subscription cancelled", key)); + logger.debug("[stream: %s] scheduleIfRequired(): Subscription cancelled".formatted(key)); } return; } if (pollState.getRequested() > 0 && !overflow.isEmpty()) { if (logger.isDebugEnabled()) { - logger.info(String.format("[stream: %s] scheduleIfRequired(): Requested: %d Emit from buffer", key, + logger.info("[stream: %s] scheduleIfRequired(): Requested: %d Emit from buffer".formatted(key, pollState.getRequested())); } emitBuffer(); @@ -290,8 +288,8 @@ private void scheduleIfRequired() { if (pollState.getRequested() == 0) { if (logger.isDebugEnabled()) { - logger.debug(String - .format("[stream: %s] scheduleIfRequired(): Subscriber has no demand; Suspending subscription", key)); + logger.debug( + "[stream: %s] scheduleIfRequired(): Subscriber has no demand; Suspending subscription".formatted(key)); } return; } @@ -303,14 +301,14 @@ private void scheduleIfRequired() { if (pollState.activateSchedule()) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] scheduleIfRequired(): Activating subscription", key)); + logger.debug("[stream: %s] scheduleIfRequired(): Activating subscription".formatted(key)); } ReadOffset readOffset = pollState.getCurrentReadOffset(); if (logger.isDebugEnabled()) { logger.debug( - String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset)); + "[stream: %s] scheduleIfRequired(): Activating subscription, offset %s".formatted(key, readOffset)); } Flux poll = readFunction.apply(readOffset) @@ -319,7 +317,7 @@ private void scheduleIfRequired() { poll.map(it -> { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, it)); + logger.debug("[stream: %s] onStreamMessage(%s)".formatted(key, it)); } pollState.updateReadOffset(it.getId().getValue()); @@ -357,7 +355,7 @@ public void onError(Throwable t) { public void onComplete() { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onComplete()", key)); + logger.debug("[stream: %s] onComplete()".formatted(key)); } pollState.scheduleCompleted(); @@ -381,20 +379,20 @@ private void onStreamMessage(V message) { if (requested == Long.MAX_VALUE) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamMessage(%s): Emitting item, fast-path", key, message)); + logger.debug("[stream: %s] onStreamMessage(%s): Emitting item, fast-path".formatted(key, message)); } sink.next(message); } else { if (pollState.decrementRequested()) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamMessage(%s): Emitting item, slow-path", key, message)); + logger.debug("[stream: %s] onStreamMessage(%s): Emitting item, slow-path".formatted(key, message)); } sink.next(message); } else { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamMessage(%s): Buffering overflow", key, message)); + logger.debug("[stream: %s] onStreamMessage(%s): Buffering overflow".formatted(key, message)); } overflow.offer(message); } @@ -403,7 +401,7 @@ private void onStreamMessage(V message) { } else { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamMessage(%s): Buffering overflow", key, message)); + logger.debug("[stream: %s] onStreamMessage(%s): Buffering overflow".formatted(key, message)); } overflow.offer(message); } @@ -412,7 +410,7 @@ private void onStreamMessage(V message) { private void onStreamError(Throwable t) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] onStreamError(%s)", key, t)); + logger.debug("[stream: %s] onStreamError(%s)".formatted(key, t)); } pollState.cancel(); @@ -435,14 +433,13 @@ private void emitBuffer() { if (message == null) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] emitBuffer(): emission missed", key)); + logger.debug("[stream: %s] emitBuffer(): emission missed".formatted(key)); } break; } if (logger.isDebugEnabled()) { - logger.debug( - String.format("[stream: %s] emitBuffer(%s): Emitting item from buffer, fast-path", key, message)); + logger.debug("[stream: %s] emitBuffer(%s): Emitting item from buffer, fast-path".formatted(key, message)); } sink.next(message); @@ -454,15 +451,14 @@ private void emitBuffer() { if (message == null) { if (logger.isDebugEnabled()) { - logger.debug(String.format("[stream: %s] emitBuffer(): emission missed", key)); + logger.debug("[stream: %s] emitBuffer(): emission missed".formatted(key)); } pollState.incrementRequested(); break; } if (logger.isDebugEnabled()) { - logger.debug( - String.format("[stream: %s] emitBuffer(%s): Emitting item from buffer, slow-path", key, message)); + logger.debug("[stream: %s] emitBuffer(%s): Emitting item from buffer, slow-path".formatted(key, message)); } sink.next(message); diff --git a/src/main/java/org/springframework/data/redis/stream/ReadOffsetStrategy.java b/src/main/java/org/springframework/data/redis/stream/ReadOffsetStrategy.java index 45cb9e8cac..2f2cc9561d 100644 --- a/src/main/java/org/springframework/data/redis/stream/ReadOffsetStrategy.java +++ b/src/main/java/org/springframework/data/redis/stream/ReadOffsetStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/stream/StreamListener.java b/src/main/java/org/springframework/data/redis/stream/StreamListener.java index 0c370c47a9..4122a2d625 100644 --- a/src/main/java/org/springframework/data/redis/stream/StreamListener.java +++ b/src/main/java/org/springframework/data/redis/stream/StreamListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/stream/StreamMessageListenerContainer.java b/src/main/java/org/springframework/data/redis/stream/StreamMessageListenerContainer.java index ff06716ac6..b54a0ce67b 100644 --- a/src/main/java/org/springframework/data/redis/stream/StreamMessageListenerContainer.java +++ b/src/main/java/org/springframework/data/redis/stream/StreamMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -94,7 +94,7 @@ *

      * RedisConnectionFactory factory = …;
      *
    - * StreamMessageListenerContainer> container = StreamMessageListenerContainer.create(factory);
    + * StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(factory);
      * Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), message -> …);
      *
      * container.start();
    @@ -106,6 +106,7 @@
      * @author Mark Paluch
      * @author Christoph Strobl
      * @author Christian Rest
    + * @author DongCheol Kim
      * @param  Stream key and Stream field type.
      * @param  Stream value type.
      * @since 2.2
    @@ -155,9 +156,10 @@ static StreamMessageListenerContainer>
     	}
     
     	/**
    -	 * Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already
    -	 * running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once
    -	 * the container is actually {@link StreamMessageListenerContainer#start() started}.
    +	 * Register a new subscription for a Redis Stream. If the container is already
    +	 * {@link StreamMessageListenerContainer#isRunning() running} the {@link Subscription} will be added and started
    +	 * immediately, otherwise it'll be scheduled and started once the container is actually
    +	 * {@link StreamMessageListenerContainer#start() started}.
     	 * 

    * Errors during {@link Record} retrieval lead to {@link Subscription#cancel() cancellation} of the underlying task. *

    @@ -174,9 +176,10 @@ default Subscription receive(StreamOffset streamOffset, StreamListener } /** - * Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already - * running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once - * the container is actually {@link StreamMessageListenerContainer#start() started}. + * Register a new subscription for a Redis Stream. If the container is already + * {@link StreamMessageListenerContainer#isRunning() running} the {@link Subscription} will be added and started + * immediately, otherwise it'll be scheduled and started once the container is actually + * {@link StreamMessageListenerContainer#start() started}. *

    * Every message must be acknowledged using * {@link org.springframework.data.redis.core.StreamOperations#acknowledge(Object, String, String...)} after @@ -200,9 +203,10 @@ default Subscription receive(Consumer consumer, StreamOffset streamOffset, St } /** - * Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already - * running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once - * the container is actually {@link StreamMessageListenerContainer#start() started}. + * Register a new subscription for a Redis Stream. If the container is already + * {@link StreamMessageListenerContainer#isRunning() running} the {@link Subscription} will be added and started + * immediately, otherwise it'll be scheduled and started once the container is actually + * {@link StreamMessageListenerContainer#start() started}. *

    * Every message is acknowledged when received. *

    @@ -223,9 +227,10 @@ default Subscription receiveAutoAck(Consumer consumer, StreamOffset streamOff } /** - * Register a new subscription for a Redis Stream. If the {@link StreamMessageListenerContainer#isRunning() is already - * running} the {@link Subscription} will be added and run immediately, otherwise it'll be scheduled and started once - * the container is actually {@link StreamMessageListenerContainer#start() started}. + * Register a new subscription for a Redis Stream. If the container is already + * {@link StreamMessageListenerContainer#isRunning() running} the {@link Subscription} will be added and started + * immediately, otherwise it'll be scheduled and started once the container is actually + * {@link StreamMessageListenerContainer#start() started}. *

    * Errors during {@link Record} are tested against test {@link StreamReadRequest#getCancelSubscriptionOnError() * cancellation predicate} whether to cancel the underlying task. @@ -245,9 +250,9 @@ default Subscription receiveAutoAck(Consumer consumer, StreamOffset streamOff /** * Unregister a given {@link Subscription} from the container. This prevents the {@link Subscription} to be restarted - * in a potential {@link SmartLifecycle#stop() stop}/{@link SmartLifecycle#start() start} scenario.
    - * An {@link Subscription#isActive() active} {@link Subscription subcription} is {@link Subscription#cancel() - * cancelled} prior to removal. + * in a potential {@link SmartLifecycle#stop() stop}/{@link SmartLifecycle#start() start} scenario. An + * {@link Subscription#isActive() active} {@link Subscription subcription} is {@link Subscription#cancel() cancelled} + * prior to removal. * * @param subscription must not be {@literal null}. */ diff --git a/src/main/java/org/springframework/data/redis/stream/StreamPollTask.java b/src/main/java/org/springframework/data/redis/stream/StreamPollTask.java index 6641a791c3..916c276478 100644 --- a/src/main/java/org/springframework/data/redis/stream/StreamPollTask.java +++ b/src/main/java/org/springframework/data/redis/stream/StreamPollTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/stream/StreamReceiver.java b/src/main/java/org/springframework/data/redis/stream/StreamReceiver.java index 3bb2f22af2..96f9591c8f 100644 --- a/src/main/java/org/springframework/data/redis/stream/StreamReceiver.java +++ b/src/main/java/org/springframework/data/redis/stream/StreamReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -90,10 +90,10 @@ *

      * ReactiveRedisConnectionFactory factory = …;
      *
    - * StreamReceiver receiver = StreamReceiver.create(factory);
    - * Flux> records = receiver.receive(StreamOffset.fromStart("my-stream"));
    + * StreamReceiver<String, String, String> receiver = StreamReceiver.create(factory);
    + * Flux<MapRecord<String, String, String>> records = receiver.receive(StreamOffset.fromStart("my-stream"));
      *
    - * recordFlux.doOnNext(record -> …);
    + * recordFlux.doOnNext(record -> …);
      * 
    * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/redis/stream/Subscription.java b/src/main/java/org/springframework/data/redis/stream/Subscription.java index d10005c25d..01db41a52f 100644 --- a/src/main/java/org/springframework/data/redis/stream/Subscription.java +++ b/src/main/java/org/springframework/data/redis/stream/Subscription.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/stream/Task.java b/src/main/java/org/springframework/data/redis/stream/Task.java index 97cf04117a..9371161ef9 100644 --- a/src/main/java/org/springframework/data/redis/stream/Task.java +++ b/src/main/java/org/springframework/data/redis/stream/Task.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/support/atomic/CompareAndSet.java b/src/main/java/org/springframework/data/redis/support/atomic/CompareAndSet.java index b361fb7714..d5682c626b 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/CompareAndSet.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/CompareAndSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/redis/support/atomic/RedisAtomicDouble.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java index 3cef647aa9..a95f78b3e5 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.support.atomic; +import java.io.Serial; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -46,6 +47,7 @@ */ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyOperations { + @Serial private static final long serialVersionUID = 1L; private volatile String key; @@ -158,11 +160,12 @@ private void initializeIfAbsent() { public double get() { Double value = operations.get(key); + if (value != null) { return value; } - throw new DataRetrievalFailureException(String.format("The key '%s' seems to no longer exist", key)); + throw new DataRetrievalFailureException("The key '%s' seems to no longer exist".formatted(key)); } /** @@ -396,6 +399,11 @@ public void rename(String newKey) { key = newKey; } + @Override + public RedisOperations getOperations() { + return generalOps; + } + @Override public int intValue() { return (int) get(); @@ -415,4 +423,5 @@ public float floatValue() { public double doubleValue() { return get(); } + } diff --git a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java index dc4289f1d2..a4f3e65dba 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,6 +15,7 @@ */ package org.springframework.data.redis.support.atomic; +import java.io.Serial; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -47,6 +48,7 @@ */ public class RedisAtomicInteger extends Number implements Serializable, BoundKeyOperations { + @Serial private static final long serialVersionUID = 1L; private volatile String key; @@ -158,11 +160,12 @@ private void initializeIfAbsent() { public int get() { Integer value = operations.get(key); + if (value != null) { return value; } - throw new DataRetrievalFailureException(String.format("The key '%s' seems to no longer exist", key)); + throw new DataRetrievalFailureException("The key '%s' seems to no longer exist".formatted(key)); } /** @@ -396,6 +399,11 @@ public void rename(String newKey) { key = newKey; } + @Override + public RedisOperations getOperations() { + return generalOps; + } + @Override public int intValue() { return get(); diff --git a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java index 9e92b0d873..2e488697ae 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,6 +15,7 @@ */ package org.springframework.data.redis.support.atomic; +import java.io.Serial; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -48,6 +49,7 @@ */ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations { + @Serial private static final long serialVersionUID = 1L; private volatile String key; @@ -159,11 +161,12 @@ private void initializeIfAbsent() { public long get() { Long value = operations.get(key); + if (value != null) { return value; } - throw new DataRetrievalFailureException(String.format("The key '%s' seems to no longer exist", key)); + throw new DataRetrievalFailureException("The key '%s' seems to no longer exist".formatted(key)); } /** @@ -393,6 +396,11 @@ public void rename(String newKey) { key = newKey; } + @Override + public RedisOperations getOperations() { + return generalOps; + } + @Override public int intValue() { return (int) get(); @@ -412,4 +420,5 @@ public float floatValue() { public double doubleValue() { return get(); } + } diff --git a/src/main/java/org/springframework/data/redis/support/collections/AbstractRedisCollection.java b/src/main/java/org/springframework/data/redis/support/collections/AbstractRedisCollection.java index d8a2af95a9..6c6f14d23d 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/AbstractRedisCollection.java +++ b/src/main/java/org/springframework/data/redis/support/collections/AbstractRedisCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -131,13 +131,6 @@ public void rename(final String newKey) { key = newKey; } - protected void checkResult(@Nullable Object obj) { - - if (obj == null) { - throw new IllegalStateException("Cannot read collection with Redis connection in pipeline/multi-exec mode"); - } - } - @Override public boolean equals(@Nullable Object o) { @@ -166,13 +159,13 @@ public int hashCode() { @Override public String toString() { + return "%s for key: %s".formatted(getClass().getSimpleName(), getKey()); + } - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%s for key:", getClass().getSimpleName())); - sb.append(getKey()); + protected void checkResult(@Nullable Object obj) { - return sb.toString(); + if (obj == null) { + throw new IllegalStateException("Cannot read collection with Redis connection in pipeline/multi-exec mode"); + } } - } diff --git a/src/main/java/org/springframework/data/redis/support/collections/CollectionUtils.java b/src/main/java/org/springframework/data/redis/support/collections/CollectionUtils.java index e5b901a022..867d1ca3be 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/CollectionUtils.java +++ b/src/main/java/org/springframework/data/redis/support/collections/CollectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/DefaultRedisList.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java index fd2da300cf..3de2317313 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -41,6 +41,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Jinbeom Kim */ public class DefaultRedisList extends AbstractRedisCollection implements RedisList { @@ -216,16 +217,14 @@ public boolean remove(Object o) { public void add(int index, E element) { if (index == 0) { - listOps.leftPush(element); - cap(); + addFirst(element); return; } int size = size(); if (index == size()) { - listOps.rightPush(element); - cap(); + addLast(element); return; } @@ -241,24 +240,15 @@ public boolean addAll(int index, Collection collection) { // insert collection in reverse if (index == 0) { - - Collection reverseCollection = CollectionUtils.reverse(collection); - - for (E element : reverseCollection) { - listOps.leftPush(element); - cap(); - } - + CollectionUtils.reverse(collection) + .forEach(this::addFirst); return true; } int size = size(); if (index == size()) { - for (E element : collection) { - listOps.rightPush(element); - cap(); - } + collection.forEach(this::addLast); return true; } @@ -341,15 +331,13 @@ public E element() { @Override public boolean offer(E element) { - listOps.rightPush(element); - cap(); - return true; + return add(element); } @Override @Nullable public E peek() { - return listOps.index(0); + return listOps.getFirst(); } @Override @@ -426,7 +414,7 @@ public E peekFirst() { @Override @Nullable public E peekLast() { - return listOps.index(-1); + return listOps.getLast(); } @Override diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java index d5cb4659ed..3233ec59ac 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisOperations; @@ -37,6 +38,7 @@ * @author Costin Leau * @author Christoph Strobl * @author Christian Bühler + * @author Tihomir Mateev */ public class DefaultRedisMap implements RedisMap { @@ -187,7 +189,6 @@ public int hashCode() { @Override public String toString() { - return "RedisStore for key:" + getKey(); } @@ -317,17 +318,27 @@ public DataType getType() { return hashOps.getType(); } + @Override + public Cursor> scan() { + return scan(ScanOptions.NONE); + } + + @Override + public BoundHashFieldExpirationOperations hashFieldExpiration() { + return hashOps.hashExpiration(); + } + + @Override + public BoundHashFieldExpirationOperations hashFieldExpiration(Collection hashFields) { + return hashOps.hashExpiration(hashFields); + } + private void checkResult(@Nullable Object obj) { if (obj == null) { throw new IllegalStateException("Cannot read collection with Redis connection in pipeline/multi-exec mode"); } } - @Override - public Cursor> scan() { - return scan(ScanOptions.NONE); - } - /** * @since 1.4 * @param options diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisSet.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisSet.java index a1370d97a4..385124f185 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/DefaultRedisZSet.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java index d6ea8ee0a5..8d1b5aed25 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisCollection.java b/src/main/java/org/springframework/data/redis/support/collections/RedisCollection.java index 5bcf57199e..1dd7195619 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisCollection.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisCollectionFactoryBean.java b/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java index a8e6228e41..b6fc3e35d2 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisIterator.java b/src/main/java/org/springframework/data/redis/support/collections/RedisIterator.java index afa0624606..10a43ef176 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisIterator.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisList.java b/src/main/java/org/springframework/data/redis/support/collections/RedisList.java index a55f1f7a03..02fb6b55ab 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisList.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisList.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java index 650114cf1d..d9c8c0a35e 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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,10 +15,13 @@ */ package org.springframework.data.redis.support.collections; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentMap; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.lang.Nullable; /** @@ -26,6 +29,8 @@ * * @author Costin Leau * @author Christoph Strobl + * @author Tihomi Mateev + * @author Mark Paluch */ public interface RedisMap extends RedisStore, ConcurrentMap { @@ -64,11 +69,44 @@ public interface RedisMap extends RedisStore, ConcurrentMap { * @since 2.6 */ @Nullable - Map.Entry randomEntry(); + Map.Entry randomEntry(); /** * @since 1.4 * @return */ Iterator> scan(); + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@link #getKey()}. Operations on the expiration object obtain keys at the time of invoking any expiration + * operation. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + BoundHashFieldExpirationOperations hashFieldExpiration(); + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@link #getKey()} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations hashFieldExpiration(K... hashFields) { + return hashFieldExpiration(Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@link #getKey()} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + BoundHashFieldExpirationOperations hashFieldExpiration(Collection hashFields); + } diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java index 61644f9787..5d3534fb6e 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.lang.Nullable; @@ -302,6 +303,17 @@ public synchronized void storeToXML(OutputStream os, String comment) throws IOEx @Override public Iterator> scan() { - throw new UnsupportedOperationException(); + return (Iterator) delegate.scan(); + } + + @Override + public BoundHashFieldExpirationOperations hashFieldExpiration() { + return (BoundHashFieldExpirationOperations) delegate.hashFieldExpiration(); } + + @Override + public BoundHashFieldExpirationOperations hashFieldExpiration(Collection hashFields) { + return (BoundHashFieldExpirationOperations) delegate.hashFieldExpiration((Collection) hashFields); + } + } diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisSet.java b/src/main/java/org/springframework/data/redis/support/collections/RedisSet.java index 60f0b0c3d1..50e0592300 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisStore.java b/src/main/java/org/springframework/data/redis/support/collections/RedisStore.java index b931b3f014..81677ab094 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisStore.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/RedisZSet.java b/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java index a0a8f5657d..63a96de44e 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/redis/support/collections/ReversedRedisListView.java b/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java index 8451a80dde..89dd3da3f0 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java +++ b/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -639,8 +639,9 @@ class DescendingListIterator implements ListIterator { DescendingListIterator(int size, int position) { if (position < 0 || position > size) { - String message = String.format("Position [%d] is out of bounds: [0, %d]", position, size); - throw new IndexOutOfBoundsException(message); + throw new IndexOutOfBoundsException( + ("Position [%d] is out of bounds;" + " position must be greater than equal to 0 and less than size %d") + .formatted(position, size)); } this.it = base.listIterator(size - position); diff --git a/src/main/java/org/springframework/data/redis/support/collections/package-info.java b/src/main/java/org/springframework/data/redis/support/collections/package-info.java index 35440023c1..9e45b4bdb3 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/package-info.java +++ b/src/main/java/org/springframework/data/redis/support/collections/package-info.java @@ -7,7 +7,7 @@ * For collections without duplicates the obvious candidate is * {@link org.springframework.data.redis.support.collections.RedisSet}. Use * {@link org.springframework.data.redis.support.collections.RedisZSet} if a certain order is required. - *

    + *

    * Lastly, for key/value associations {@link org.springframework.data.redis.support.collections.RedisMap} providing a * Map-like abstraction on top of a Redis hash. */ diff --git a/src/main/java/org/springframework/data/redis/util/ByteUtils.java b/src/main/java/org/springframework/data/redis/util/ByteUtils.java index 93e4e76969..6ae97539ed 100644 --- a/src/main/java/org/springframework/data/redis/util/ByteUtils.java +++ b/src/main/java/org/springframework/data/redis/util/ByteUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/util/RedisAssertions.java b/src/main/java/org/springframework/data/redis/util/RedisAssertions.java index 34e5bfede7..22da3c395d 100644 --- a/src/main/java/org/springframework/data/redis/util/RedisAssertions.java +++ b/src/main/java/org/springframework/data/redis/util/RedisAssertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,7 +25,9 @@ * * @author John Blum * @since 3.1.0 + * @deprecated since 3.3, will be removed in a future revision in favor of Spring's {@link Assert} utility. */ +@Deprecated(since = "3.3", forRemoval = true) public abstract class RedisAssertions { /** @@ -40,7 +42,7 @@ public abstract class RedisAssertions { * @see #requireNonNull(Object, Supplier) */ public static T requireNonNull(@Nullable T target, String message, Object... arguments) { - return requireNonNull(target, () -> String.format(message, arguments)); + return requireNonNull(target, () -> message.formatted(arguments)); } /** @@ -88,7 +90,7 @@ public static T requireNonNull(@Nullable T target, RuntimeExceptionSupplier * @see #requireNonNull(Object, Supplier) */ public static T requireState(@Nullable T target, String message, Object... arguments) { - return requireState(target, () -> String.format(message, arguments)); + return requireState(target, () -> message.formatted(arguments)); } /** diff --git a/src/main/kotlin/org/springframework/data/redis/core/PartialUpdateExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/PartialUpdateExtensions.kt index c36c8d7710..ced500d4f8 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/PartialUpdateExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/PartialUpdateExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensions.kt index 492af3d9a4..bba2924256 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensions.kt index fe26ed4d66..b7a1594921 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensions.kt index ebbf651325..6a4e38036a 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensions.kt index b58a93ebcd..ffd6fee07e 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt index 0878ae0a8b..4fef76f849 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensions.kt index bd1d4db2c4..8594699ec2 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensions.kt index 8bba7683c0..579b7fadd7 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensions.kt index 2bdda370d6..44a4a82922 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt index dcacae65de..e0679e2834 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensions.kt index de0ed8a657..e53512eb74 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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 5b4c9ef0b7..48afb51a3f 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Redis 3.3 RC1 (2024.0.0) +Spring Data Redis 4.0 M3 (2025.1.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -10,47 +10,3 @@ code for the these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/test/java/org/springframework/data/redis/Address.java b/src/test/java/org/springframework/data/redis/Address.java index 8628d84ca3..f24a9c6d1d 100644 --- a/src/test/java/org/springframework/data/redis/Address.java +++ b/src/test/java/org/springframework/data/redis/Address.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java b/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java index b3d5315d29..1a6a249cca 100644 --- a/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/ConnectionFactoryTracker.java b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java index d619e6021c..2210c99ae7 100644 --- a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java +++ b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/DoubleAsStringObjectFactory.java b/src/test/java/org/springframework/data/redis/DoubleAsStringObjectFactory.java index 99d5ddea98..9cd21baef5 100644 --- a/src/test/java/org/springframework/data/redis/DoubleAsStringObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/DoubleAsStringObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/DoubleObjectFactory.java b/src/test/java/org/springframework/data/redis/DoubleObjectFactory.java index 2d2ab66c57..279437ea72 100644 --- a/src/test/java/org/springframework/data/redis/DoubleObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/DoubleObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/LongAsStringObjectFactory.java b/src/test/java/org/springframework/data/redis/LongAsStringObjectFactory.java index 9ee9af93c8..fc183fafdf 100644 --- a/src/test/java/org/springframework/data/redis/LongAsStringObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/LongAsStringObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/LongObjectFactory.java b/src/test/java/org/springframework/data/redis/LongObjectFactory.java index da538b0f6d..773fcad57b 100644 --- a/src/test/java/org/springframework/data/redis/LongObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/LongObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/ObjectFactory.java b/src/test/java/org/springframework/data/redis/ObjectFactory.java index 3438d57ffa..44d0343b34 100644 --- a/src/test/java/org/springframework/data/redis/ObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/ObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/Person.java b/src/test/java/org/springframework/data/redis/Person.java index 0201e14a59..610257d7a0 100644 --- a/src/test/java/org/springframework/data/redis/Person.java +++ b/src/test/java/org/springframework/data/redis/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/PersonObjectFactory.java b/src/test/java/org/springframework/data/redis/PersonObjectFactory.java index d429823a4c..8b5ccb1523 100644 --- a/src/test/java/org/springframework/data/redis/PersonObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/PersonObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java b/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java index d4b4b34525..c3c5da29c9 100644 --- a/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/PropertyEditorsIntegrationTests.java b/src/test/java/org/springframework/data/redis/PropertyEditorsIntegrationTests.java index 7eae9bdc85..733d742695 100644 --- a/src/test/java/org/springframework/data/redis/PropertyEditorsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/PropertyEditorsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/RawObjectFactory.java b/src/test/java/org/springframework/data/redis/RawObjectFactory.java index fd0148881f..9d1608f17b 100644 --- a/src/test/java/org/springframework/data/redis/RawObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/RawObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/RedisViewPE.java b/src/test/java/org/springframework/data/redis/RedisViewPE.java index d4b7e25853..65f4ffbc6b 100644 --- a/src/test/java/org/springframework/data/redis/RedisViewPE.java +++ b/src/test/java/org/springframework/data/redis/RedisViewPE.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/SettingsUtils.java b/src/test/java/org/springframework/data/redis/SettingsUtils.java index cadeca6ff7..19548d72a0 100644 --- a/src/test/java/org/springframework/data/redis/SettingsUtils.java +++ b/src/test/java/org/springframework/data/redis/SettingsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Properties; import org.springframework.data.redis.connection.RedisClusterConfiguration; @@ -114,9 +115,11 @@ public static RedisStandaloneConfiguration standaloneConfiguration() { * @return a new {@link RedisSentinelConfiguration} initialized with test endpoint settings. */ public static RedisSentinelConfiguration sentinelConfiguration() { - return new RedisSentinelConfiguration(getSentinelMaster(), - new HashSet<>(Arrays.asList(String.format("%s:%d", getHost(), getSentinelPort()), - String.format("%s:%d", getHost(), getSentinelPort() + 1)))); + + List sentinelHostPorts = List.of("%s:%d".formatted(getHost(), getSentinelPort()), + "%s:%d".formatted(getHost(), getSentinelPort() + 1)); + + return new RedisSentinelConfiguration(getSentinelMaster(), new HashSet<>(sentinelHostPorts)); } /** @@ -125,8 +128,7 @@ public static RedisSentinelConfiguration sentinelConfiguration() { * @return a new {@link RedisClusterConfiguration} initialized with test endpoint settings. */ public static RedisClusterConfiguration clusterConfiguration() { - return new RedisClusterConfiguration( - Collections.singletonList(String.format("%s:%d", getHost(), getClusterPort()))); + return new RedisClusterConfiguration(List.of("%s:%d".formatted(getHost(), getClusterPort()))); } /** diff --git a/src/test/java/org/springframework/data/redis/StringObjectFactory.java b/src/test/java/org/springframework/data/redis/StringObjectFactory.java index 609d9405e9..37d0ae3e12 100644 --- a/src/test/java/org/springframework/data/redis/StringObjectFactory.java +++ b/src/test/java/org/springframework/data/redis/StringObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/TestCondition.java b/src/test/java/org/springframework/data/redis/TestCondition.java index 3eceff67d4..6b17ea91b5 100644 --- a/src/test/java/org/springframework/data/redis/TestCondition.java +++ b/src/test/java/org/springframework/data/redis/TestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/cache/CacheTestParams.java b/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java index 538a059d33..b037099902 100644 --- a/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java +++ b/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/cache/DefaultCacheStatisticsCollectorUnitTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollectorUnitTests.java index bdef135de1..ae6fcafab1 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollectorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultCacheStatisticsCollectorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/DefaultRedisCachWriterUnitTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java index 233333ef83..e4b0696a34 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,10 +16,13 @@ package org.springframework.data.redis.cache; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -32,9 +35,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - +import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisKeyCommands; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.types.Expiration; @@ -42,6 +46,7 @@ * Unit tests for {@link DefaultRedisCacheWriter} * * @author John Blum + * @author Christoph Strobl */ @ExtendWith(MockitoExtension.class) class DefaultRedisCacheWriterUnitTests { @@ -109,4 +114,28 @@ void getWithNullTtl() { verify(this.mockConnection, times(1)).close(); verifyNoMoreInteractions(this.mockConnection, mockStringCommands); } + + @Test // GH-2890 + void mustNotUnlockWhenLockingFails() { + + byte[] key = "TestKey".getBytes(); + byte[] value = "TestValue".getBytes(); + + RedisStringCommands mockStringCommands = mock(RedisStringCommands.class); + RedisKeyCommands mockKeyCommands = mock(RedisKeyCommands.class); + + doReturn(mockStringCommands).when(this.mockConnection).stringCommands(); + doReturn(mockKeyCommands).when(this.mockConnection).keyCommands(); + doThrow(new PessimisticLockingFailureException("you-shall-not-pass")).when(mockStringCommands).set(any(byte[].class), + any(byte[].class), any(), any()); + + RedisCacheWriter cacheWriter = spy( + new DefaultRedisCacheWriter(this.mockConnectionFactory, Duration.ofMillis(10), mock(BatchStrategy.class)) + .withStatisticsCollector(this.mockCacheStatisticsCollector)); + + assertThatException() + .isThrownBy(() -> cacheWriter.get("TestCache", key, () -> value, Duration.ofMillis(10), false)); + + verify(mockKeyCommands, never()).del(any()); + } } diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java index 1f07ea6110..6b74b80931 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -33,6 +33,7 @@ import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -242,6 +243,22 @@ void putIfAbsentShouldAddExpiringEntryWhenKeyDoesNotExist() { assertThat(writer.getCacheStatistics(CACHE_NAME).getPuts()).isOne(); } + @ParameterizedRedisTest // GH-2890 + void getWithValueLoaderShouldStoreCacheValue() { + + RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) + .withStatisticsCollector(CacheStatisticsCollector.create()); + + writer.get(CACHE_NAME, binaryCacheKey, () -> binaryCacheValue, Duration.ofSeconds(5), true); + + doWithConnection(connection -> { + assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(3).isLessThan(6); + }); + + assertThat(writer.getCacheStatistics(CACHE_NAME).getMisses()).isOne(); + assertThat(writer.getCacheStatistics(CACHE_NAME).getPuts()).isOne(); + } + @ParameterizedRedisTest // DATAREDIS-481, DATAREDIS-1082 void removeShouldDeleteEntry() { @@ -427,6 +444,7 @@ void noOpStatisticsCollectorReturnsEmptyStatsInstance() { } @ParameterizedRedisTest // GH-1686 + @Disabled("Occasional failures on CI but not locally") void doLockShouldGetLock() throws InterruptedException { int threadCount = 3; @@ -437,13 +455,12 @@ void doLockShouldGetLock() throws InterruptedException { DefaultRedisCacheWriter cw = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(10), BatchStrategies.keys()) { - boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) { + void doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) { - boolean doLock = super.doLock(name, contextualKey, contextualValue, connection); + super.doLock(name, contextualKey, contextualValue, connection); // any concurrent access (aka not waiting until the lock is acquired) will result in a concurrency greater 1 assertThat(concurrency.incrementAndGet()).isOne(); - return doLock; } @Nullable diff --git a/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java index 9ef7231ae7..d718055ffa 100644 --- a/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/cache/MutableCacheStatisticsUnitTests.java b/src/test/java/org/springframework/data/redis/cache/MutableCacheStatisticsUnitTests.java index b80358ea9e..29c9e1002f 100644 --- a/src/test/java/org/springframework/data/redis/cache/MutableCacheStatisticsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/MutableCacheStatisticsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/cache/RedisCacheConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java index edbe99e964..94de42115d 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java index 699d07917d..e03714beb2 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index 6677ab7a9d..520b73d09f 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,9 +16,6 @@ package org.springframework.data.redis.cache; import static org.assertj.core.api.Assertions.*; -import static org.awaitility.Awaitility.*; - -import io.netty.util.concurrent.DefaultThreadFactory; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -29,19 +26,16 @@ import java.util.Date; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; + import org.springframework.cache.Cache.ValueWrapper; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.cache.interceptor.SimpleKeyGenerator; @@ -251,6 +245,37 @@ void getShouldReturnValueWrapperHoldingNullIfNullValueStored() { assertThat(result.get()).isEqualTo(null); } + @ParameterizedRedisTest // GH-2890 + void getWithValueLoaderShouldStoreNull() { + + doWithConnection(connection -> connection.set(binaryCacheKey, binaryNullValue)); + + Object result = cache.get(key, () -> { + throw new IllegalStateException(); + }); + + assertThat(result).isNull(); + } + + @ParameterizedRedisTest // GH-2890 + void getWithValueLoaderShouldRetrieveValue() { + + AtomicLong counter = new AtomicLong(); + Object result = cache.get(key, () -> { + counter.incrementAndGet(); + return sample; + }); + + assertThat(result).isEqualTo(sample); + result = cache.get(key, () -> { + counter.incrementAndGet(); + return sample; + }); + + assertThat(result).isEqualTo(sample); + assertThat(counter).hasValue(1); + } + @ParameterizedRedisTest // DATAREDIS-481 void evictShouldRemoveKey() { @@ -358,7 +383,7 @@ void prefixCacheNameCreatesCacheKeyCorrectly() { doWithConnection(connection -> assertThat( connection.stringCommands().get("redis::cache::key-1".getBytes(StandardCharsets.UTF_8))) - .isEqualTo(binarySample)); + .isEqualTo(binarySample)); } @ParameterizedRedisTest // DATAREDIS-715 @@ -435,105 +460,6 @@ void cacheShouldFailOnNonConvertibleCacheKey() { assertThatIllegalStateException().isThrownBy(() -> cache.put(key, sample)); } - @ParameterizedRedisTest // GH-2079 - void multipleThreadsLoadValueOnce() throws InterruptedException { - - int threadCount = 2; - - CountDownLatch prepare = new CountDownLatch(threadCount); - CountDownLatch prepareForReturn = new CountDownLatch(1); - CountDownLatch finished = new CountDownLatch(threadCount); - AtomicInteger retrievals = new AtomicInteger(); - AtomicReference storage = new AtomicReference<>(); - - cache = new RedisCache("foo", new RedisCacheWriter() { - - @Override - public byte[] get(String name, byte[] key) { - return get(name, key, null); - } - - @Override - public byte[] get(String name, byte[] key, @Nullable Duration ttl) { - - prepare.countDown(); - try { - prepareForReturn.await(1, TimeUnit.MINUTES); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - - return storage.get(); - } - - @Override - public CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl) { - byte[] value = get(name, key); - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl) { - return null; - } - - @Override - public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { - storage.set(value); - } - - @Override - public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { - return new byte[0]; - } - - @Override - public void remove(String name, byte[] key) { - - } - - @Override - public void clean(String name, byte[] pattern) { - - } - - @Override - public void clearStatistics(String name) { - - } - - @Override - public RedisCacheWriter withStatisticsCollector(CacheStatisticsCollector cacheStatisticsCollector) { - return null; - } - - @Override - public CacheStatistics getCacheStatistics(String cacheName) { - return null; - } - }, RedisCacheConfiguration.defaultCacheConfig()); - - ThreadPoolExecutor tpe = new ThreadPoolExecutor(threadCount, threadCount, 1, TimeUnit.MINUTES, - new LinkedBlockingDeque<>(), new DefaultThreadFactory("RedisCacheTests")); - - IntStream.range(0, threadCount).forEach(it -> tpe.submit(() -> { - cache.get("foo", retrievals::incrementAndGet); - finished.countDown(); - })); - - // wait until all Threads have arrived in RedisCacheWriter.get(…) - prepare.await(); - - // let all threads continue - prepareForReturn.countDown(); - - // wait until ThreadPoolExecutor has completed. - finished.await(); - tpe.shutdown(); - - assertThat(retrievals).hasValue(1); - } - @EnabledOnCommand("GETEX") @ParameterizedRedisTest // GH-2351 void cacheGetWithTimeToIdleExpirationWhenEntryNotExpiredShouldReturnValue() { @@ -545,11 +471,10 @@ void cacheGetWithTimeToIdleExpirationWhenEntryNotExpiredShouldReturnValue() { assertThat(unwrap(cache.get(this.key))).isEqualTo(this.sample); - for (int count = 0; count < 5; count++) { + doWithConnection(connection -> { - await().atMost(Duration.ofMillis(100)); - assertThat(unwrap(cache.get(this.key))).isEqualTo(this.sample); - } + assertThat(connection.keyCommands().ttl(this.binaryCacheKey)).isGreaterThan(1); + }); } @EnabledOnCommand("GETEX") @@ -563,9 +488,9 @@ void cacheGetWithTimeToIdleExpirationAfterEntryExpiresShouldReturnNull() { assertThat(unwrap(cache.get(this.key))).isEqualTo(this.sample); - await().atMost(Duration.ofMillis(200)); - - assertThat(cache.get(this.cacheKey, Person.class)).isNull(); + doWithConnection(connection -> { + assertThat(connection.keyCommands().ttl(this.binaryCacheKey)).isGreaterThan(1); + }); } @ParameterizedRedisTest // GH-2650 @@ -600,6 +525,30 @@ void retrieveReturnsCachedValue() throws Exception { assertThat(value.get(5, TimeUnit.SECONDS)).isNotNull(); assertThat(value.get().get()).isEqualTo(this.sample); assertThat(value).isDone(); + + doWithConnection(connection -> { + assertThat(connection.keyCommands().ttl(this.binaryCacheKey)).isEqualTo(-1); + }); + } + + @ParameterizedRedisTest // GH-2890 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void retrieveAppliesTimeToIdle() throws ExecutionException, InterruptedException { + + doWithConnection(connection -> connection.stringCommands().set(this.binaryCacheKey, this.binarySample)); + + RedisCache cache = new RedisCache("cache", usingRedisCacheWriter(), + usingRedisCacheConfiguration(withTtiExpiration())); + + CompletableFuture value = cache.retrieve(this.key); + + assertThat(value).isNotNull(); + assertThat(value.get().get()).isEqualTo(this.sample); + assertThat(value).isDone(); + + doWithConnection(connection -> { + assertThat(connection.keyCommands().ttl(this.binaryCacheKey)).isGreaterThan(1); + }); } @ParameterizedRedisTest // GH-2650 @@ -756,7 +705,7 @@ private Object unwrap(@Nullable Object value) { private Function withTtiExpiration() { Function entryTtlFunction = cacheConfiguration -> cacheConfiguration - .entryTtl(Duration.ofMillis(100)); + .entryTtl(Duration.ofSeconds(10)); return entryTtlFunction.andThen(RedisCacheConfiguration::enableTimeToIdle); } diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java index 94fae0870f..b1b0ef311f 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/cache/RedisCacheWriterUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java index e9d6ce75ff..ec8e69b52c 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/config/NamespaceIntegrationTests.java b/src/test/java/org/springframework/data/redis/config/NamespaceIntegrationTests.java index c02fe6be4b..4334ce1028 100644 --- a/src/test/java/org/springframework/data/redis/config/NamespaceIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/config/NamespaceIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/config/PropertyEditorSupportIntegrationTests.java b/src/test/java/org/springframework/data/redis/config/PropertyEditorSupportIntegrationTests.java index 2c88f2a3b3..9ccac8b555 100644 --- a/src/test/java/org/springframework/data/redis/config/PropertyEditorSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/config/PropertyEditorSupportIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under 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/redis/config/StubErrorHandler.java b/src/test/java/org/springframework/data/redis/config/StubErrorHandler.java index 2c43190c89..93b6ea9455 100644 --- a/src/test/java/org/springframework/data/redis/config/StubErrorHandler.java +++ b/src/test/java/org/springframework/data/redis/config/StubErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 1d96b83a2f..288aa202b0 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -45,6 +45,9 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Range; @@ -96,6 +99,7 @@ import org.springframework.data.redis.test.condition.RedisDriver; import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.util.Streamable; +import org.springframework.util.ObjectUtils; /** * Base test class for AbstractConnection integration tests @@ -110,6 +114,8 @@ * @author Andrey Shlykov * @author Hendrik Duerkop * @author Shyngys Sapraliyev + * @author Roman Osadchuk + * @author Tihomir Mateev */ public abstract class AbstractConnectionIntegrationTests { @@ -129,7 +135,8 @@ public abstract class AbstractConnectionIntegrationTests { protected List actual = new ArrayList<>(); - @Autowired @EnabledOnRedisDriver.DriverQualifier protected RedisConnectionFactory connectionFactory; + @Autowired + @EnabledOnRedisDriver.DriverQualifier protected RedisConnectionFactory connectionFactory; protected RedisConnection byteConnection; @@ -185,6 +192,23 @@ void testExpire() { await().atMost(Duration.ofMillis(3000L)).until(keyExpired::passes); } + @LongRunningTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void testExpireWithArgs() { + + actual.add(connection.set("exp", "true")); + actual.add( + connection.applyExpiration("exp".getBytes(), Expiration.from(Duration.ofMinutes(1)), ExpirationOptions.none())); + actual.add(connection.applyExpiration("exp".getBytes(), Expiration.from(Duration.ofMinutes(1)), + ExpirationOptions.builder().nx().build())); + actual.add(connection.applyExpiration("exp".getBytes(), Expiration.from(Duration.ofMinutes(2)), + ExpirationOptions.builder().gt().build())); + actual.add(connection.applyExpiration("exp".getBytes(), Expiration.from(Duration.ofMinutes(3)), + ExpirationOptions.builder().lt().build())); + + verifyResults(Arrays.asList(true, true, false, true, false)); + } + @Test // DATAREDIS-1103 void testSetWithKeepTTL() { @@ -585,8 +609,7 @@ public void testNullKey() { try { connection.decr((String) null); fail("Decrement should fail with null key"); - } catch (Exception expected) { - } + } catch (Exception expected) {} } @Test @@ -598,8 +621,7 @@ public void testNullValue() { try { connection.append(key, null); fail("Append should fail with null value"); - } catch (DataAccessException expected) { - } + } catch (DataAccessException expected) {} } @Test @@ -610,8 +632,7 @@ public void testHashNullKey() { try { connection.hExists(key, null); fail("hExists should fail with null key"); - } catch (DataAccessException expected) { - } + } catch (DataAccessException expected) {} } @Test @@ -624,8 +645,7 @@ public void testHashNullValue() { try { connection.hSet(key, field, null); fail("hSet should fail with null value"); - } catch (DataAccessException expected) { - } + } catch (DataAccessException expected) {} } @Test @@ -662,8 +682,7 @@ public void testPubSubWithNamedChannels() throws Exception { try { Thread.sleep(500); - } catch (InterruptedException ignore) { - } + } catch (InterruptedException ignore) {} // open a new connection RedisConnection connection2 = connectionFactory.getConnection(); @@ -706,8 +725,7 @@ public void testPubSubWithPatterns() throws Exception { try { Thread.sleep(500); - } catch (InterruptedException ignore) { - } + } catch (InterruptedException ignore) {} // open a new connection RedisConnection connection2 = connectionFactory.getConnection(); @@ -753,6 +771,63 @@ void testExecute() { assertThat(stringSerializer.deserialize((byte[]) getResults().get(1))).isEqualTo("bar"); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void testExecuteExpirationWithConditions() { + + actual.add(connection.set("foo", "bar")); + actual.add(connection.execute("TTL", "foo")); + actual.add(connection.execute("EXPIRE", "foo", "100", "NX")); + actual.add(connection.execute("PERSIST", "foo")); + actual.add(connection.execute("TTL", "foo")); + + List results = getResults(); + + assertThat(results.get(1)).isEqualTo(-1L); + assertThat(results.get(2)).isIn(1L, true); + assertThat(results.get(3)).isIn(1L, true); + assertThat(results.get(4)).isEqualTo(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExecuteHashFieldExpiration() { + + actual.add(connection.hSet("foo", "bar", "field")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HEXPIRE", "foo", "100", "NX", "FIELDS", "1", "bar")); + actual.add(connection.execute("HPERSIST", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + + List results = getResults(); + + assertThat(deserializeList(results, 1, stringSerializer)).containsOnly(-1L); + assertThat(deserializeList(results, 2, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 3, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 4, stringSerializer)).containsOnly(-1L); + } + + List deserializeList(List objects, int index, RedisSerializer serializer) { + + List result = new ArrayList<>(); + Object o = objects.get(index); + if (o instanceof List ls) { + for (Object obj : ls) { + + if (obj instanceof byte[]) { + result.add(serializer.deserialize((byte[]) obj)); + } else { + result.add(obj); + } + } + + return result; + } + + throw new IllegalArgumentException( + "Object at index " + index + " is not a list but " + ObjectUtils.nullSafeToString(o)); + } + @Test void testExecuteNoArgs() { @@ -2638,8 +2713,7 @@ void scanShouldReadEntireValueRange() { void scanWithType() { assumeThat(isPipelinedOrQueueingConnection(connection)) - .describedAs("SCAN is only available in non-pipeline | non-queueing mode") - .isFalse(); + .describedAs("SCAN is only available in non-pipeline | non-queueing mode").isFalse(); connection.set("key", "data"); connection.lPush("list", "foo"); @@ -2666,7 +2740,8 @@ private static List toList(Cursor cursor) { public void scanShouldReadEntireValueRangeWhenIndividualScanIterationsReturnEmptyCollection() { byteConnection.openPipeline(); - IntStream.range(0, 100).forEach(it -> byteConnection.stringCommands().set("key:%s".formatted(it).getBytes(StandardCharsets.UTF_8), "data".getBytes(StandardCharsets.UTF_8))); + IntStream.range(0, 100).forEach(it -> byteConnection.stringCommands() + .set("key:%s".formatted(it).getBytes(StandardCharsets.UTF_8), "data".getBytes(StandardCharsets.UTF_8))); byteConnection.closePipeline(); Cursor cursor = connection.scan(ScanOptions.scanOptions().match("key*9").count(10).build()); @@ -3361,8 +3436,7 @@ void geoSearchShouldConsiderDistanceCorrectly() { actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); actual.add( - connection.geoSearch(key, GeoReference.fromMember(PALERMO), - GeoShape.byRadius(new Distance(200, KILOMETERS)), + connection.geoSearch(key, GeoReference.fromMember(PALERMO), GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoSearchArgs().limit(2).includeDistance().includeCoordinates())); List results = getResults(); @@ -3380,8 +3454,7 @@ void geoSearchStoreByMemberShouldStoreResult() { actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); actual.add(connection.geoSearchStore("georesults", key, GeoReference.fromMember(PALERMO), - GeoShape.byRadius(new Distance(200, KILOMETERS)), - newGeoSearchStoreArgs().limit(2).storeDistance())); + GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoSearchStoreArgs().limit(2).storeDistance())); actual.add(connection.zScore("georesults", PALERMO.getName())); actual.add(connection.zScore("georesults", ARIGENTO.getName())); @@ -3399,8 +3472,7 @@ void geoSearchStoreByPointShouldStoreResult() { actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); actual.add(connection.geoSearchStore("georesults", key, GeoReference.fromCoordinate(PALERMO), - GeoShape.byRadius(new Distance(200, KILOMETERS)), - newGeoSearchStoreArgs().limit(2).storeDistance())); + GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoSearchStoreArgs().limit(2).storeDistance())); actual.add(connection.zScore("georesults", PALERMO.getName())); actual.add(connection.zScore("georesults", ARIGENTO.getName())); @@ -3437,6 +3509,221 @@ void hStrLenReturnsZeroWhenKeyDoesNotExist() { verifyResults(Arrays.asList(new Object[] { 0L })); } + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsSuccessAndSetsTTL() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hExpire("hash-hexpire", 5L, "key-2")); + actual.add(connection.hTtl("hash-hexpire", "key-2")); + + List results = getResults(); + assertThat(results.get(0)).isEqualTo(Boolean.TRUE); + assertThat((List) results.get(1)).contains(1L); + assertThat((List) results.get(2)).allSatisfy(value -> assertThat((Long) value).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hExpire("hash-hexpire", 5L, "missking-field")); + actual.add(connection.hExpire("missing-key", 5L, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-2L), List.of(-2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsTwoWhenZeroProvided() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hExpire("hash-hexpire", 0, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPEXPIRE") + public void hpExpireReturnsSuccessAndSetsTTL() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hpExpire("hash-hexpire", 5000L, "key-2")); + actual.add(connection.hpTtl("hash-hexpire", "key-2")); + + List results = getResults(); + assertThat(results.get(0)).isEqualTo(Boolean.TRUE); + assertThat((List) results.get(1)).contains(1L); + assertThat((List) results.get(2)).allSatisfy(value -> assertThat((Long) value).isBetween(0L, 5000L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HPEXPIRE") + public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hpExpire("hash-hexpire", 5L, "missing-field")); + actual.add(connection.hpExpire("missing-key", 5L, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-2L), List.of(-2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPEXPIRE") + public void hpExpireReturnsTwoWhenZeroProvided() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hpExpire("hash-hexpire", 0, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIREAT") + public void hExpireAtReturnsSuccessAndSetsTTL() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + actual.add(connection.hExpireAt("hash-hexpire", inFiveSeconds, "key-2")); + actual.add(connection.hTtl("hash-hexpire", "key-2")); + + List results = getResults(); + assertThat(results.get(0)).isEqualTo(Boolean.TRUE); + assertThat((List) results.get(1)).contains(1L); + assertThat((List) results.get(2)).allSatisfy(value -> assertThat((Long) value).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIREAT") + public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + actual.add(connection.hpExpire("hash-hexpire", inFiveSeconds, "missing-field")); + actual.add(connection.hpExpire("missing-key", inFiveSeconds, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-2L), List.of(-2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIREAT") + public void hExpireAtReturnsTwoWhenZeroProvided() { + + long fiveSecondsAgo = Instant.now().minusSeconds(5L).getEpochSecond(); + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hExpireAt("hash-hexpire", fiveSecondsAgo, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIREAT") + public void hpExpireAtReturnsSuccessAndSetsTTL() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + actual.add(connection.hpExpireAt("hash-hexpire", inFiveSeconds, "key-2")); + actual.add(connection.hTtl("hash-hexpire", "key-2")); + + List results = getResults(); + assertThat(results.get(0)).isEqualTo(Boolean.TRUE); + assertThat((List) results.get(1)).contains(1L); + assertThat((List) results.get(2)).allSatisfy(value -> assertThat((Long) value).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIREAT") + public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + actual.add(connection.hpExpireAt("hash-hexpire", inFiveSeconds, "missing-field")); + actual.add(connection.hpExpireAt("missing-key", inFiveSeconds, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-2L), List.of(-2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPEXPIREAT") + public void hpExpireAdReturnsTwoWhenZeroProvided() { + + long fiveSecondsAgo = Instant.now().minusSeconds(5L).getEpochSecond(); + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hpExpireAt("hash-hexpire", fiveSecondsAgo, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPERSIST") + public void hPersistReturnsSuccessAndPersistsField() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hExpire("hash-hexpire", 5L, "key-2")); + actual.add(connection.hPersist("hash-hexpire", "key-2")); + actual.add(connection.hTtl("hash-hexpire", "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(1L), List.of(1L), List.of(-1L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPERSIST") + public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hPersist("hash-hexpire", "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-1L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HPERSIST") + public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hPersist("hash-hexpire", "missing-field")); + actual.add(connection.hPersist("missing-key", "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-2L), List.of(-2L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HTTL") + public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hTtl("hash-hexpire", "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-1L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HTTL") + public void hTtlReturnsMinusIndependendOfTimeUnitOneWhenFieldHasNoExpiration() { + + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); + actual.add(connection.hTtl("hash-hexpire", TimeUnit.HOURS, "key-2")); + + verifyResults(Arrays.asList(Boolean.TRUE, List.of(-1L))); + } + + @Test // GH-3054 + @EnabledOnCommand("HTTL") + public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() { + + actual.add(connection.hTtl("hash-hexpire", "missing-field")); + actual.add(connection.hTtl("missing-key", "key-2")); + + verifyResults(Arrays.asList(new Object[] { List.of(-2L), List.of(-2L) })); + } + @Test // DATAREDIS-694 void touchReturnsNrOfKeysTouched() { @@ -3577,6 +3864,22 @@ void bitFieldIncrByWithOverflowShouldWorkCorrectly() { assertThat(results.get(3)).isNotNull(); } + @ParameterizedTest // GH-2903 + @ValueSource(booleans = { false, true }) + void bitFieldIncrByAndThenGetShouldWorkCorrectly(boolean isMultipliedByTypeLengthOffset) { + + var offset = isMultipliedByTypeLengthOffset ? BitFieldSubCommands.Offset.offset(300L).multipliedByTypeLength() + : BitFieldSubCommands.Offset.offset(400L); + + actual.add(connection.bitfield(KEY_1, create().incr(INT_8).valueAt(offset).by(1L))); + actual.add(connection.bitfield(KEY_1, create().get(INT_8).valueAt(offset))); + + List results = getResults(); + + // should return same results after INCRBY and GET operations for bitfield with same offset + assertThat(results).containsExactly(List.of(1L), List.of(1L)); + } + @Test // DATAREDIS-562 void bitfieldShouldAllowMultipleSubcommands() { @@ -3864,8 +4167,8 @@ public void xPendingShouldLoadPendingMessagesForConsumer() { actual.add(connection.xReadGroupAsString(Consumer.from("my-group", "my-consumer"), StreamOffset.create(KEY_1, ReadOffset.lastConsumed()))); - actual.add(connection.xPending(KEY_1, "my-group", "my-consumer", - org.springframework.data.domain.Range.unbounded(), 10L)); + actual.add( + connection.xPending(KEY_1, "my-group", "my-consumer", org.springframework.data.domain.Range.unbounded(), 10L)); List results = getResults(); assertThat(results).hasSize(4); @@ -4086,7 +4389,7 @@ public void xinfoConsumersNoConsumer() { assertThat(info.size()).isZero(); } - @Test //GH-2345 + @Test // GH-2345 public void zRangeStoreByScoreStoresKeys() { String dstKey = KEY_2; String srcKey = KEY_1; @@ -4124,7 +4427,7 @@ public void zRangeStoreRevByScoreStoresKeys() { assertThat((LinkedHashSet) result.get(5)).containsSequence(VALUE_3, VALUE_4); } - @Test //GH-2345 + @Test // GH-2345 public void zRangeStoreByLexStoresKeys() { String dstKey = KEY_2; String srcKey = KEY_1; diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java index f30dc5f108..f1205d2848 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/AbstractConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionTransactionIntegrationTests.java index 1b5c4672f5..52e8836655 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionTransactionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -134,13 +134,6 @@ public void testWatchWhileInTx() { .isThrownBy(() -> connection.watch("foo".getBytes())); } - @Test - public void testScriptKill() { - // Impossible to call script kill in a tx because you can't issue the - // exec command while Redis is running a script - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> connection.scriptKill()); - } - @Test // DATAREDIS-417 @Disabled @Override diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionUnitTestBase.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionUnitTestBase.java index 050ffaee56..40c3b9fa88 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionUnitTestBase.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionUnitTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -55,8 +55,7 @@ private Class resolveReturnedClassFromGernericType() { private ParameterizedType resolveReturnedClassFromGernericType(Class clazz) { Object genericSuperclass = clazz.getGenericSuperclass(); - if (genericSuperclass instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + if (genericSuperclass instanceof ParameterizedType parameterizedType) { Type rawtype = parameterizedType.getRawType(); if (AbstractConnectionUnitTestBase.class.equals(rawtype)) { return parameterizedType; diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java b/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java index 1c63fbdd3e..f99df546be 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/BitFieldSubCommandsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/BitFieldSubCommandsUnitTests.java index 693ef624bc..c7876e4092 100644 --- a/src/test/java/org/springframework/data/redis/connection/BitFieldSubCommandsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/BitFieldSubCommandsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java index b3a6d5f33e..f57a2f5233 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index 2d13df6131..193ac17190 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,7 +119,7 @@ public interface ClusterConnectionTests { void expireAtShouldBeSetCorrectly(); // DATAREDIS-315 - void expireShouldBeSetCorreclty(); + void expireShouldBeSetCorrectly(); // DATAREDIS-315 void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb(); @@ -386,7 +386,7 @@ public interface ClusterConnectionTests { void pExpireAtShouldBeSetCorrectly(); // DATAREDIS-315 - void pExpireShouldBeSetCorreclty(); + void pExpireShouldBeSetCorrectly(); // DATAREDIS-315 void pSetExShouldSetValueCorrectly(); diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java index f48d44ce8d..fa2b4706b5 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ void localCalculationShouldMatchServers() { Long serverSlot = jedis.clusterKeySlot(key); assertThat(slot) - .as(String.format("Expected slot for key '%s' to be %s but server calculated %s.", key, slot, serverSlot)) + .describedAs("Expected slot for key '%s' to be %s but server calculated %s", key, slot, serverSlot) .isEqualTo(serverSlot.intValue()); } diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java b/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java index cb33e10de2..4be52a6363 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/DefaultStringRedisConnectionPipelineTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java index 74ad0677e9..40c28082e1 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java index 3f9f4a0cbb..e7a0e6c607 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index 9931ac330d..5e9c1b38e6 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -273,28 +273,28 @@ public void testExists() { @Test public void testExpireBytes() { - doReturn(true).when(nativeConnection).expire(fooBytes, 1L); + doReturn(true).when(nativeConnection).expire(fooBytes, 1L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.expire(fooBytes, 1L)); verifyResults(Collections.singletonList(true)); } @Test public void testExpire() { - doReturn(true).when(nativeConnection).expire(fooBytes, 1L); + doReturn(true).when(nativeConnection).expire(fooBytes, 1L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.expire(foo, 1L)); verifyResults(Collections.singletonList(true)); } @Test public void testExpireAtBytes() { - doReturn(true).when(nativeConnection).expireAt(fooBytes, 1L); + doReturn(true).when(nativeConnection).expireAt(fooBytes, 1L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.expireAt(fooBytes, 1L)); verifyResults(Collections.singletonList(true)); } @Test public void testExpireAt() { - doReturn(true).when(nativeConnection).expireAt(fooBytes, 1L); + doReturn(true).when(nativeConnection).expireAt(fooBytes, 1L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.expireAt(foo, 1L)); verifyResults(Collections.singletonList(true)); } @@ -1662,28 +1662,28 @@ public void testZUnionStore() { @Test public void testPExpireBytes() { - doReturn(true).when(nativeConnection).pExpire(fooBytes, 34L); + doReturn(true).when(nativeConnection).pExpire(fooBytes, 34L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.pExpire(fooBytes, 34L)); verifyResults(Collections.singletonList(true)); } @Test public void testPExpire() { - doReturn(true).when(nativeConnection).pExpire(fooBytes, 34L); + doReturn(true).when(nativeConnection).pExpire(fooBytes, 34L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.pExpire(foo, 34L)); verifyResults(Collections.singletonList(true)); } @Test public void testPExpireAtBytes() { - doReturn(true).when(nativeConnection).pExpireAt(fooBytes, 34L); + doReturn(true).when(nativeConnection).pExpireAt(fooBytes, 34L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.pExpireAt(fooBytes, 34L)); verifyResults(Collections.singletonList(true)); } @Test public void testPExpireAt() { - doReturn(true).when(nativeConnection).pExpireAt(fooBytes, 34L); + doReturn(true).when(nativeConnection).pExpireAt(fooBytes, 34L, ExpirationOptions.Condition.ALWAYS); actual.add(connection.pExpireAt(foo, 34L)); verifyResults(Collections.singletonList(true)); } diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java index 5f7db6e388..3441a8f581 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/ReactiveStreamCommandsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/ReactiveStreamCommandsUnitTests.java new file mode 100644 index 0000000000..dedaf0c84d --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/ReactiveStreamCommandsUnitTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024-2025 the original author 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.redis.connection; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveStreamCommands.PendingRecordsCommand; + +/** + * Unit tests for {@link ReactiveStreamCommands}. + * + * @author jinkshower + */ +class ReactiveStreamCommandsUnitTests { + + @Test // GH-2982 + void pendingRecordsCommandRangeShouldThrowExceptionWhenRangeIsNull() { + + ByteBuffer key = ByteBuffer.wrap("my-stream".getBytes()); + String groupName = "my-group"; + + PendingRecordsCommand command = PendingRecordsCommand.pending(key, groupName); + + assertThatIllegalArgumentException().isThrownBy(() -> command.range(null, 10L)); + } + + @Test // GH-2982 + void pendingRecordsCommandRangeShouldThrowExceptionWhenCountIsNegative() { + + ByteBuffer key = ByteBuffer.wrap("my-stream".getBytes()); + String groupName = "my-group"; + + PendingRecordsCommand command = PendingRecordsCommand.pending(key, groupName); + Range range = Range.closed("0", "10"); + + assertThatIllegalArgumentException().isThrownBy(() -> command.range(range, -1L)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java index 6d969d0876..ffc3bde6e8 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,7 +41,6 @@ class RedisClusterConfigurationUnitTests { private static final String HOST_AND_PORT_3 = "localhost:789"; private static final String HOST_AND_PORT_4 = "[fe80::a00:27ff:fe4b:ee48]:6379"; private static final String HOST_AND_PORT_5 = "[fe80:1234:1a2b:0:27ff:fe4b:0:ee48]:6380"; - private static final String HOST_AND_NO_PORT = "localhost"; @Test // DATAREDIS-315 void shouldCreateRedisClusterConfigurationCorrectly() { @@ -75,12 +74,6 @@ void shouldCreateRedisClusterConfigurationCorrectlyGivenMultipleHostAndPortStrin new RedisNode("localhost", 789)); } - @Test // DATAREDIS-315 - void shouldThrowExecptionOnInvalidHostAndPortString() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new RedisClusterConfiguration(Collections.singleton(HOST_AND_NO_PORT))); - } - @Test // DATAREDIS-315 void shouldThrowExceptionWhenListOfHostAndPortIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new RedisClusterConfiguration(Collections.singleton(null))); diff --git a/src/test/java/org/springframework/data/redis/connection/RedisClusterNodeSlotRangeUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisClusterNodeSlotRangeUnitTests.java index 0345e8bfb5..034a06f38c 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisClusterNodeSlotRangeUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisClusterNodeSlotRangeUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index 2a10866752..068e87444a 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -1093,6 +1093,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op return delegate.set(key, value, expiration, options); } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return delegate.setGet(key, value, expiration, option); + } + @Override public List bitField(byte[] key, BitFieldSubCommands subCommands) { return delegate.bitField(key, subCommands); diff --git a/src/test/java/org/springframework/data/redis/connection/RedisElastiCacheConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisElastiCacheConfigurationUnitTests.java index d0940d99c6..8d64a7bbbf 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisElastiCacheConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisElastiCacheConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/RedisNodeUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisNodeUnitTests.java new file mode 100644 index 0000000000..0cef0df5ed --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/RedisNodeUnitTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024-2025 the original author 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.redis.connection; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Unit tests for {@link RedisNode}. + * + * @author LeeHyungGeol + * @author Mark Paluch + */ +class RedisNodeUnitTests { + + @Test // GH-2928 + void shouldParseIPv4AddressWithPort() { + + RedisNode node = RedisNode.fromString("127.0.0.1:1234"); + + assertThat(node.getHost()).isEqualTo("127.0.0.1"); + assertThat(node.getPort()).isEqualTo(1234); + } + + @ParameterizedTest // GH-2928 + @ValueSource(strings = { "127.0.0.1", "127.0.0.1:" }) + void shouldParseIPv4AddressWithoutPort(String source) { + + RedisNode node = RedisNode.fromString(source); + + assertThat(node.getHost()).isEqualTo("127.0.0.1"); + assertThat(node.getPort()).isEqualTo(RedisNode.DEFAULT_PORT); + } + + @Test // GH-2928 + void shouldParseIPv6AddressWithPort() { + + RedisNode node = RedisNode.fromString("[aaaa:bbbb::dddd:eeee]:1234"); + + assertThat(node.getHost()).isEqualTo("aaaa:bbbb::dddd:eeee"); + assertThat(node.getPort()).isEqualTo(1234); + } + + @ParameterizedTest // GH-2928 + @ValueSource(strings = { "[aaaa:bbbb::dddd:eeee]", "[aaaa:bbbb::dddd:eeee]:" }) + void shouldParseIPv6AddressWithoutPort(String source) { + + RedisNode node = RedisNode.fromString(source); + + assertThat(node.getHost()).isEqualTo("aaaa:bbbb::dddd:eeee"); + assertThat(node.getPort()).isEqualTo(RedisNode.DEFAULT_PORT); + } + + @Test // GH-2928 + void shouldParseBareHostnameWithPort() { + + RedisNode node = RedisNode.fromString("my.redis.server:6379"); + + assertThat(node.getHost()).isEqualTo("my.redis.server"); + assertThat(node.getPort()).isEqualTo(6379); + } + + @ParameterizedTest // GH-2928 + @ValueSource(strings = { "my.redis.server", "[my.redis.server:" }) + void shouldParseBareHostnameWithoutPort(String source) { + + RedisNode node = RedisNode.fromString("my.redis.server"); + + assertThat(node.getHost()).isEqualTo("my.redis.server"); + assertThat(node.getPort()).isEqualTo(RedisNode.DEFAULT_PORT); + } + + @Test // GH-2928 + void shouldThrowExceptionForInvalidPort() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> RedisNode.fromString("127.0.0.1:invalidPort")); + } + + @Test // GH-2928 + void shouldParseBareIPv6WithoutPort() { + + RedisNode node = RedisNode.fromString("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + + assertThat(node.getHost()).isEqualTo("2001:0db8:85a3:0000:0000:8a2e:0370"); + assertThat(node.getPort()).isEqualTo(7334); + } + +} + diff --git a/src/test/java/org/springframework/data/redis/connection/RedisPasswordUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisPasswordUnitTests.java index ac5c87748f..78f8c7df6b 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisPasswordUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisPasswordUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/RedisSentinelConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java index 2b38c0a751..9f9adb4e30 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,7 +41,6 @@ class RedisSentinelConfigurationUnitTests { private static final String HOST_AND_PORT_1 = "127.0.0.1:123"; private static final String HOST_AND_PORT_2 = "localhost:456"; private static final String HOST_AND_PORT_3 = "localhost:789"; - private static final String HOST_AND_NO_PORT = "localhost"; @Test // DATAREDIS-372 void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndSingleHostAndPortString() { @@ -74,12 +73,6 @@ void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndMultipleHostAn new RedisNode("localhost", 789)); } - @Test // DATAREDIS-372 - void shouldThrowExecptionOnInvalidHostAndPortString() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new RedisSentinelConfiguration("mymaster", Collections.singleton(HOST_AND_NO_PORT))); - } - @Test // DATAREDIS-372 void shouldThrowExceptionWhenListOfHostAndPortIsNull() { assertThatIllegalArgumentException() diff --git a/src/test/java/org/springframework/data/redis/connection/RedisServerUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisServerUnitTests.java index 215f56f374..f8ebab1183 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisServerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisServerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/RedisStreamCommandsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisStreamCommandsUnitTests.java new file mode 100644 index 0000000000..74579d57a6 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/RedisStreamCommandsUnitTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024-2025 the original author 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.redis.connection; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisStreamCommands.XPendingOptions; + +/** + * Unit tests for {@link RedisStreamCommands}. + * + * @author jinkshower + */ +class RedisStreamCommandsUnitTests { + + @Test // GH-2982 + void xPendingOptionsUnboundedShouldThrowExceptionWhenCountIsNegative() { + assertThatIllegalArgumentException().isThrownBy(() -> XPendingOptions.unbounded(-1L)); + } + + @Test // GH-2982 + void xPendingOptionsRangeShouldThrowExceptionWhenRangeIsNull() { + assertThatIllegalArgumentException().isThrownBy(() -> XPendingOptions.range(null, 10L)); + } + + @Test // GH-2982 + void xPendingOptionsRangeShouldThrowExceptionWhenCountIsNegative() { + + Range range = Range.closed("0", "10"); + + assertThatIllegalArgumentException().isThrownBy(() -> XPendingOptions.range(range, -1L)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/RedisZSetCommandsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisZSetCommandsUnitTests.java index 28774e5156..9ebf89e6ea 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisZSetCommandsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisZSetCommandsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/connection/ReturnTypeUnitTests.java b/src/test/java/org/springframework/data/redis/connection/ReturnTypeUnitTests.java index 4c51047f9a..fb5c24bf16 100644 --- a/src/test/java/org/springframework/data/redis/connection/ReturnTypeUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ReturnTypeUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -34,8 +34,19 @@ class ReturnTypeUnitTests { @ParameterizedTest // DATAREDIS-1245 @ValueSource(classes = { List.class, ArrayList.class, LinkedList.class }) void shouldConsiderListsAsMultiType(Class listClass) { - assertThat(ReturnType.fromJavaType(listClass)).isEqualTo(ReturnType.MULTI); } + @ParameterizedTest // GH-3090 + @ValueSource(classes = { Integer.class, Long.class, Number.class }) + void shouldConsiderIntegerType(Class listClass) { + assertThat(ReturnType.fromJavaType(listClass)).isEqualTo(ReturnType.INTEGER); + } + + @ParameterizedTest // GH-3090 + @ValueSource(classes = { Double.class, Float.class, String.class }) + void shouldConsiderValueType(Class listClass) { + assertThat(ReturnType.fromJavaType(listClass)).isEqualTo(ReturnType.VALUE); + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/StreamRecordsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/StreamRecordsUnitTests.java index d1073dc31b..02ab8faf5a 100644 --- a/src/test/java/org/springframework/data/redis/connection/StreamRecordsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/StreamRecordsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/WeightsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/WeightsUnitTests.java index beabaad473..adc1b8e7bd 100644 --- a/src/test/java/org/springframework/data/redis/connection/WeightsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/WeightsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java index 74d3f7df8d..6c7c248558 100644 --- a/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,18 +18,18 @@ import static org.assertj.core.api.Assertions.*; import java.util.Iterator; -import java.util.regex.Matcher; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisNode.NodeType; -import org.springframework.data.redis.connection.convert.Converters.ClusterNodesConverter; +import org.springframework.data.redis.connection.convert.Converters.ClusterNodesConverter.AddressPortHostname; /** * Unit tests for {@link Converters}. @@ -37,6 +37,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Sorokin Evgeniy + * @author Marcin Grzejszczak */ class ConvertersUnitTests { @@ -72,6 +73,10 @@ class ConvertersUnitTests { private static final String CLUSTER_NODE_WITH_SINGLE_INVALID_IPV6_HOST = "67adfe3df1058896e3cb49d2863e0f70e7e159fa 2a02:6b8:c67:9c:0:6d8b:33da:5a2c: master,nofailover - 0 1692108412315 1 connected 0-5460"; + private static final String CLUSTER_NODE_WITH_SINGLE_IPV4_EMPTY_HOSTNAME = "3765733728631672640db35fd2f04743c03119c6 10.180.0.33:11003@16379, master - 0 1708041426947 2 connected 0-5460"; + + private static final String CLUSTER_NODE_WITH_SINGLE_IPV4_HOSTNAME = "3765733728631672640db35fd2f04743c03119c6 10.180.0.33:11003@16379,hostname1 master - 0 1708041426947 2 connected 0-5460"; + @Test // DATAREDIS-315 void toSetOfRedis30ClusterNodesShouldConvertSingleStringNodesResponseCorrectly() { @@ -248,6 +253,37 @@ void toClusterNodeWithIPv6Hostname() { assertThat(node.getSlotRange().getSlots().size()).isEqualTo(5461); } + @Test // GH-2862 + void toClusterNodeWithIPv4EmptyHostname() { + + RedisClusterNode node = Converters.toClusterNode(CLUSTER_NODE_WITH_SINGLE_IPV4_EMPTY_HOSTNAME); + + assertThat(node.getId()).isEqualTo("3765733728631672640db35fd2f04743c03119c6"); + assertThat(node.getHost()).isEqualTo("10.180.0.33"); + assertThat(node.hasValidHost()).isTrue(); + assertThat(node.getPort()).isEqualTo(11003); + assertThat(node.getType()).isEqualTo(NodeType.MASTER); + assertThat(node.getFlags()).contains(Flag.MASTER); + assertThat(node.getLinkState()).isEqualTo(LinkState.CONNECTED); + assertThat(node.getSlotRange().getSlots().size()).isEqualTo(5461); + } + + @Test // GH-2862 + void toClusterNodeWithIPv4Hostname() { + + RedisClusterNode node = Converters.toClusterNode(CLUSTER_NODE_WITH_SINGLE_IPV4_HOSTNAME); + + assertThat(node.getId()).isEqualTo("3765733728631672640db35fd2f04743c03119c6"); + assertThat(node.getHost()).isEqualTo("10.180.0.33"); + assertThat(node.getName()).isEqualTo("hostname1"); + assertThat(node.hasValidHost()).isTrue(); + assertThat(node.getPort()).isEqualTo(11003); + assertThat(node.getType()).isEqualTo(NodeType.MASTER); + assertThat(node.getFlags()).contains(Flag.MASTER); + assertThat(node.getLinkState()).isEqualTo(LinkState.CONNECTED); + assertThat(node.getSlotRange().getSlots().size()).isEqualTo(5461); + } + @Test // GH-2678 void toClusterNodeWithIPv6HostnameSquareBrackets() { @@ -271,35 +307,64 @@ void toClusterNodeWithInvalidIPv6Hostname() { @ParameterizedTest // GH-2678 @MethodSource("clusterNodesEndpoints") - void shouldAcceptHostPatterns(String endpoint, String expectedAddress, String expectedPort, String expectedHostname) { + void shouldAcceptHostPatterns(String endpoint, AddressPortHostname expected) { - Matcher matcher = ClusterNodesConverter.clusterEndpointPattern.matcher(endpoint); - assertThat(matcher.matches()).isTrue(); + AddressPortHostname addressPortHostname = AddressPortHostname.parse(endpoint); - assertThat(matcher.group(1)).isEqualTo(expectedAddress); - assertThat(matcher.group(2)).isEqualTo(expectedPort); - assertThat(matcher.group(3)).isEqualTo(expectedHostname); + assertThat(addressPortHostname).isEqualTo(expected); } static Stream clusterNodesEndpoints() { - return Stream.of( + Stream regular = Stream.of( // IPv4 with Host, Redis 3 - Arguments.of("1.2.4.4:7379", "1.2.4.4", "7379", null), + Arguments.of("1.2.4.4:7379", new AddressPortHostname("1.2.4.4", "7379", null)), // IPv6 with Host, Redis 3 - Arguments.of("6b8:c67:9c:0:6d8b:33da:5a2c:6380", "6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null), + Arguments.of("6b8:c67:9c:0:6d8b:33da:5a2c:6380", + new AddressPortHostname("6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null)), // Assuming IPv6 in brackets with Host, Redis 3 - Arguments.of("[6b8:c67:9c:0:6d8b:33da:5a2c]:6380", "6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null), + Arguments.of("[6b8:c67:9c:0:6d8b:33da:5a2c]:6380", + new AddressPortHostname("6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null)), // IPv4 with Host and Bus Port, Redis 4 - Arguments.of("127.0.0.1:7382@17382", "127.0.0.1", "7382", null), + Arguments.of("127.0.0.1:7382@17382", new AddressPortHostname("127.0.0.1", "7382", null)), // IPv6 with Host and Bus Port, Redis 4 - Arguments.of("6b8:c67:9c:0:6d8b:33da:5a2c:6380", "6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null), + Arguments.of("6b8:c67:9c:0:6d8b:33da:5a2c:6380", + new AddressPortHostname("6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null)), // Hostname with Port and Bus Port, Redis 7 - Arguments.of("my.host-name.com:7379@17379", "my.host-name.com", "7379", null), + Arguments.of("my.host-name.com:7379@17379", new AddressPortHostname("my.host-name.com", "7379", null)), // With hostname, Redis 7 - Arguments.of("1.2.4.4:7379@17379,my.host-name.com", "1.2.4.4", "7379", "my.host-name.com")); + Arguments.of("1.2.4.4:7379@17379,my.host-name.com", + new AddressPortHostname("1.2.4.4", "7379", "my.host-name.com"))); + + Stream weird = Stream.of( + // Port-only + Arguments.of(":6380", new AddressPortHostname("", "6380", null)), + + // Port-only with bus-port + Arguments.of(":6380@6381", new AddressPortHostname("", "6380", null)), + // IP with trailing comma + Arguments.of("127.0.0.1:6380,", new AddressPortHostname("127.0.0.1", "6380", null)), + // IPv6 with bus-port + Arguments.of("2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380@6381", + new AddressPortHostname("2a02:6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null)), + // IPv6 with bus-port and hostname + Arguments.of("2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380@6381,hostname1", + new AddressPortHostname("2a02:6b8:c67:9c:0:6d8b:33da:5a2c", "6380", "hostname1")), + // Port-only with hostname + Arguments.of(":6380,hostname1", new AddressPortHostname("", "6380", "hostname1")), + + // Port-only with bus-port + Arguments.of(":6380@6381,hostname1", new AddressPortHostname("", "6380", "hostname1")), + // IPv6 in brackets with bus-port + Arguments.of("[2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380@6381", + new AddressPortHostname("2a02:6b8:c67:9c:0:6d8b:33da:5a2c", "6380", null)), + // IPv6 in brackets with bus-port and hostname + Arguments.of("[2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380@6381,hostname1", + new AddressPortHostname("2a02:6b8:c67:9c:0:6d8b:33da:5a2c", "6380", "hostname1"))); + + return Stream.concat(regular, weird); } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java index 38dd34c0b2..ade77dd515 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/jedis/JedisClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java index ccb41b5ff9..74203c8bc1 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 0ebc8ea86d..b4229d9522 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,6 +16,7 @@ package org.springframework.data.redis.connection.jedis; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.data.Offset.*; import static org.assertj.core.data.Offset.offset; import static org.springframework.data.redis.connection.BitFieldSubCommands.*; import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*; @@ -36,6 +37,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; import java.util.*; import java.util.concurrent.TimeUnit; @@ -43,6 +45,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range.Bound; @@ -50,20 +53,13 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.BitFieldSubCommands; -import org.springframework.data.redis.connection.ClusterConnectionTests; -import org.springframework.data.redis.connection.ClusterSlotHashUtil; -import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.connection.DefaultSortParameters; +import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.Limit; -import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisServerCommands.FlushOption; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; -import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding; import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; @@ -75,12 +71,14 @@ import org.springframework.data.redis.test.condition.EnabledOnRedisClusterAvailable; import org.springframework.data.redis.test.extension.JedisExtension; import org.springframework.data.redis.test.util.HexStringUtils; +import org.springframework.test.util.ReflectionTestUtils; /** * @author Christoph Strobl * @author Mark Paluch * @author Pavel Khokhlov * @author Dennis Neufeld + * @author Tihomir Mateev */ @EnabledOnRedisClusterAvailable @ExtendWith(JedisExtension.class) @@ -438,8 +436,24 @@ public void expireAtShouldBeSetCorrectly() { assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void expireAtWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000, + ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000, + ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 15000, + ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 - public void expireShouldBeSetCorreclty() { + public void expireShouldBeSetCorrectly() { nativeConnection.set(KEY_1, VALUE_1); @@ -448,6 +462,19 @@ public void expireShouldBeSetCorreclty() { assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void expireWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 public void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb() { @@ -1035,6 +1062,183 @@ public void hStrLenReturnsZeroWhenKeyDoesNotExist() { assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_1_BYTES)).isEqualTo(0L); } + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + // missing field + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hExpire(KEY_2_BYTES, 5L, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5000L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isBetween(0L, 5000L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + // missing field + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hpExpire(KEY_2_BYTES, 5L, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAtReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + // missing field + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_2_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAdReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireAtReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isBetween(0L, 5000L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + // missing field + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_2_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireAdReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsSuccessAndPersistsField() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L); + assertThat(clusterConnection.hashCommands().hPersist(KEY_3_BYTES, KEY_2_BYTES)).contains(-2L); + + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() { + + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_3_BYTES, KEY_2_BYTES)).contains(-2L); + } + @Test // DATAREDIS-315 public void hValsShouldRetrieveValuesCorrectly() { @@ -1183,7 +1387,7 @@ public void blMoveShouldMoveElementsCorrectly() { .isEqualTo(VALUE_2_BYTES); assertThat( clusterConnection.bLMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT, 0.01)) - .isNull(); + .isNull(); assertThat(nativeConnection.lrange(SAME_SLOT_KEY_1, 0, -1)).isEmpty(); assertThat(nativeConnection.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_2, VALUE_3); @@ -1386,8 +1590,27 @@ public void pExpireAtShouldBeSetCorrectly() { assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void pExpireAtWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000, ExpirationOptions.Condition.XX)) + .isFalse(); + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000, ExpirationOptions.Condition.NX)) + .isTrue(); + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 15000, ExpirationOptions.Condition.LT)) + .isFalse(); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 - public void pExpireShouldBeSetCorreclty() { + public void pExpireShouldBeSetCorrectly() { nativeConnection.set(KEY_1, VALUE_1); @@ -1396,6 +1619,19 @@ public void pExpireShouldBeSetCorreclty() { assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void pExpireWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 public void pSetExShouldSetValueCorrectly() { @@ -2783,13 +3019,13 @@ void bitFieldIncrByWithOverflowShouldWorkCorrectly() { assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().incr(unsigned(2)).valueAt(BitFieldSubCommands.Offset.offset(102L)).overflow(FAIL).by(1L))) - .containsExactly(1L); + .containsExactly(1L); assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().incr(unsigned(2)).valueAt(BitFieldSubCommands.Offset.offset(102L)).overflow(FAIL).by(1L))) - .containsExactly(2L); + .containsExactly(2L); assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().incr(unsigned(2)).valueAt(BitFieldSubCommands.Offset.offset(102L)).overflow(FAIL).by(1L))) - .containsExactly(3L); + .containsExactly(3L); assertThat(clusterConnection.stringCommands() .bitField(JedisConverters.toBytes(KEY_1), create().incr(unsigned(2)).valueAt(BitFieldSubCommands.Offset.offset(102L)).overflow(FAIL).by(1L)) @@ -2801,7 +3037,7 @@ void bitfieldShouldAllowMultipleSubcommands() { assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().incr(signed(5)).valueAt(BitFieldSubCommands.Offset.offset(100L)).by(1L).get(unsigned(4)).valueAt(0L))) - .containsExactly(1L, 0L); + .containsExactly(1L, 0L); } @Test // DATAREDIS-562 @@ -2811,13 +3047,13 @@ void bitfieldShouldWorkUsingNonZeroBasedOffset() { clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().set(INT_8).valueAt(BitFieldSubCommands.Offset.offset(0L).multipliedByTypeLength()).to(100L) .set(INT_8).valueAt(BitFieldSubCommands.Offset.offset(1L).multipliedByTypeLength()).to(200L))) - .containsExactly(0L, 0L); + .containsExactly(0L, 0L); assertThat( clusterConnection.stringCommands() .bitField(JedisConverters.toBytes(KEY_1), create().get(INT_8).valueAt(BitFieldSubCommands.Offset.offset(0L).multipliedByTypeLength()).get(INT_8) - .valueAt(BitFieldSubCommands.Offset.offset(1L).multipliedByTypeLength()))).containsExactly(100L, - -56L); + .valueAt(BitFieldSubCommands.Offset.offset(1L).multipliedByTypeLength()))) + .containsExactly(100L, -56L); } @Test // DATAREDIS-1005 @@ -2950,4 +3186,21 @@ void lPosNonExisting() { assertThat(result).isEmpty(); } + + @Test // GH-2986 + void shouldUseCachedTopology() { + + JedisClusterConnection.JedisClusterTopologyProvider provider = (JedisClusterConnection.JedisClusterTopologyProvider) clusterConnection + .getTopologyProvider(); + ReflectionTestUtils.setField(provider, "cached", null); + + ClusterTopology topology = provider.getTopology(); + assertThat(topology).isInstanceOf(JedisClusterConnection.JedisClusterTopology.class); + + assertThat(provider.shouldUseCachedValue(null)).isFalse(); + assertThat(provider.shouldUseCachedValue( + new JedisClusterConnection.JedisClusterTopology(Set.of(), System.currentTimeMillis() - 101, 100))).isFalse(); + assertThat(provider.shouldUseCachedValue( + new JedisClusterConnection.JedisClusterTopology(Set.of(), System.currentTimeMillis() + 100, 100))).isTrue(); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java index 7a341018a7..161561415e 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java index 1a3a3d6bd1..6a75f265b1 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java index f6e3f3f9e4..d766502a8a 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -18,9 +18,13 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.util.Pool; import java.io.IOException; import java.security.NoSuchAlgorithmException; @@ -263,6 +267,28 @@ void shouldApplyClientConfiguration() throws NoSuchAlgorithmException { assertThat(connectionFactory.getPoolConfig()).isSameAs(poolConfig); } + @Test // GH-3072 + void shouldInitializePool() throws Exception { + + JedisPoolConfig poolConfig = new JedisPoolConfig(); + Pool poolMock = mock(Pool.class); + + JedisClientConfiguration configuration = JedisClientConfiguration.builder() // + .usePooling().poolConfig(poolConfig) // + .build(); + + connectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration(), configuration) { + @Override + protected Pool createRedisPool() { + return poolMock; + } + }; + + connectionFactory.afterPropertiesSet(); + + verify(poolMock).preparePool(); + } + @Test // DATAREDIS-574 void shouldReturnStandaloneConfiguration() { @@ -338,6 +364,30 @@ void afterPropertiesTriggersConnectionInitialization() { assertThat(connectionFactory.isRunning()).isTrue(); } + @Test // GH-3007 + void clientConfigurationAppliesCustomizer() { + + JedisClientConfig resp3Config = apply( + JedisClientConfiguration.builder().customize(DefaultJedisClientConfig.Builder::resp3).build()); + + assertThat(resp3Config.getRedisProtocol()).isEqualTo(RedisProtocol.RESP3); + + JedisClientConfig resp2Config = apply( + JedisClientConfiguration.builder().customize(it -> it.protocol(RedisProtocol.RESP2)).build()); + + assertThat(resp2Config.getRedisProtocol()).isEqualTo(RedisProtocol.RESP2); + } + + private static JedisClientConfig apply(JedisClientConfiguration configuration) { + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration(), + configuration); + connectionFactory.setEarlyStartup(false); + connectionFactory.afterPropertiesSet(); + + return (JedisClientConfig) ReflectionTestUtils.getField(connectionFactory, "clientConfig"); + } + @Test // GH-2866 void earlyStartupDoesNotStartConnectionFactory() { @@ -356,12 +406,12 @@ void earlyStartupDoesNotStartConnectionFactory() { private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfiguration, @Nullable JedisPoolConfig poolConfig) { + Pool poolMock = mock(Pool.class); // we have to use a spy here as jedis would start connecting to redis sentinels when the pool is created. JedisConnectionFactory connectionFactorySpy = spy(new JedisConnectionFactory(sentinelConfiguration, poolConfig)); - doReturn(null).when(connectionFactorySpy).createRedisSentinelPool(any(RedisSentinelConfiguration.class)); - - doReturn(null).when(connectionFactorySpy).createRedisPool(); + doReturn(poolMock).when(connectionFactorySpy).createRedisSentinelPool(any(RedisSentinelConfiguration.class)); + doReturn(poolMock).when(connectionFactorySpy).createRedisPool(); return connectionFactorySpy; } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java index 21446b5644..b4aae693c2 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -404,8 +404,8 @@ void pExpireShouldSupportExiprationForValuesLargerThanInteger() { long ttl = connection.pTtl("pexpireKey"); assertThat(millis - ttl < 20L) - .as(String.format("difference between millis=%s and ttl=%s should not be greater than 20ms but is %s", millis, - ttl, millis - ttl)) + .describedAs("difference between millis=%s and ttl=%s should not be greater than 20ms but is %s", + millis, ttl, millis - ttl) .isTrue(); } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java index a5cacfb2df..2e34011f4f 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -37,6 +37,7 @@ * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch + * @author Ivan Kripakov */ @ExtendWith(SpringExtension.class) @ContextConfiguration("JedisConnectionIntegrationTests-context.xml") @@ -75,97 +76,6 @@ public void testClosePoolPipelinedDbSelect() { } // Unsupported Ops - @Test - public void testScriptLoadEvalSha() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testScriptLoadEvalSha); - } - - @Test - public void testEvalShaArrayStrings() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalShaArrayStrings); - } - - @Test - public void testEvalShaArrayBytes() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalShaArrayBytes); - } - - @Test - @Disabled - public void testEvalShaNotFound() {} - - @Test - @Disabled - public void testEvalShaArrayError() {} - - @Test - public void testEvalReturnString() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnString); - } - - @Test - public void testEvalReturnNumber() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnNumber); - } - - @Test - public void testEvalReturnSingleOK() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnSingleOK); - } - - @Test - @Disabled - public void testEvalReturnSingleError() {} - - @Test - public void testEvalReturnFalse() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnFalse); - } - - @Test - public void testEvalReturnTrue() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnTrue); - } - - @Test - public void testEvalReturnArrayStrings() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnArrayStrings); - } - - @Test - public void testEvalReturnArrayNumbers() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnArrayNumbers); - } - - @Test - public void testEvalReturnArrayOKs() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnArrayOKs); - } - - @Test - public void testEvalReturnArrayFalses() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnArrayFalses); - } - - @Test - public void testEvalReturnArrayTrues() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testEvalReturnArrayTrues); - } - - @Test - public void testScriptExists() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::testScriptExists); - } - - @Test - public void testScriptKill() { - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> connection.scriptKill()); - } - - @Test - @Disabled - public void testScriptFlush() {} - @Test // DATAREDIS-269 public void clientSetNameWorksCorrectly() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(super::clientSetNameWorksCorrectly); diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionTransactionIntegrationTests.java index 45706608a2..7e08465475 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionTransactionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -36,6 +36,7 @@ * * @author Jennifer Hickey * @author Mark Paluch + * @author Ivan Kripakov */ @ExtendWith(SpringExtension.class) @ContextConfiguration("JedisConnectionIntegrationTests-context.xml") @@ -56,91 +57,44 @@ public void tearDown() { @Disabled("Jedis issue: Transaction tries to return String instead of List") public void testGetConfig() {} - // Unsupported Ops - @Test - @Disabled - public void testScriptLoadEvalSha() {} - - @Test - @Disabled - public void testEvalShaArrayStrings() {} - @Test - @Disabled - public void testEvalShaArrayBytes() {} - - @Test - @Disabled - public void testEvalShaNotFound() {} + public void testEvalShaNotFound() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + getResults(); + }); + } @Test public void testEvalShaArrayError() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> connection.evalSha("notasha", ReturnType.MULTI, 1, "key1", "arg1")); + .isThrownBy(() -> { + connection.evalSha("notasha", ReturnType.MULTI, 1, "key1", "arg1"); + getResults(); + }); } @Test public void testEvalArrayScriptError() { assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> connection.eval("return {1,2", ReturnType.MULTI, 1, "foo", "bar")); + .isThrownBy(() -> { + connection.eval("return {1,2", ReturnType.MULTI, 1, "foo", "bar"); + getResults(); + }); } @Test - @Disabled - public void testEvalReturnString() {} - - @Test - @Disabled - public void testEvalReturnNumber() {} - - @Test - @Disabled - public void testEvalReturnSingleOK() {} - - @Test - @Disabled - public void testEvalReturnSingleError() {} - - @Test - @Disabled - public void testEvalReturnFalse() {} - - @Test - @Disabled - public void testEvalReturnTrue() {} - - @Test - @Disabled - public void testEvalReturnArrayStrings() {} - - @Test - @Disabled - public void testEvalReturnArrayNumbers() {} - - @Test - @Disabled - public void testEvalReturnArrayOKs() {} - - @Test - @Disabled - public void testEvalReturnArrayFalses() {} - - @Test - @Disabled - public void testEvalReturnArrayTrues() {} - - @Test - @Disabled - public void testScriptExists() {} - - @Test - @Disabled - public void testScriptKill() {} + public void testEvalReturnSingleError() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(()-> { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + getResults(); + }); + } - @Test - @Disabled - public void testScriptFlush() {} + // Unsupported Ops @Test @Disabled public void testInfoBySection() {} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTests.java index 4a9150d4de..348576b710 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 21c1b8e41d..5a869da105 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java index b83593891b..92019da91e 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/jedis/JedisSentinelConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java index e7ec289152..1289e7f6dd 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java index cd036204ae..06150da2d4 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisSubscriptionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSubscriptionUnitTests.java index 262c185d59..34b249fe79 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSubscriptionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSubscriptionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/jedis/JedisTransactionalConnectionStarvationTest.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisTransactionalConnectionStarvationTest.java index fb4e4057fe..0e2d2e9061 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisTransactionalConnectionStarvationTest.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisTransactionalConnectionStarvationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/jedis/ScanTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/ScanTests.java index 269cf395a5..c89882bd49 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/ScanTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/ScanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/jedis/TransactionalJedisIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/TransactionalJedisIntegrationTests.java index e5c79be848..437976e41b 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/TransactionalJedisIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/TransactionalJedisIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/jedis/extension/JedisConnectionFactoryExtension.java b/src/test/java/org/springframework/data/redis/connection/jedis/extension/JedisConnectionFactoryExtension.java index 07b59cd38a..267e5cb37b 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/extension/JedisConnectionFactoryExtension.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/extension/JedisConnectionFactoryExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/LettuceAclIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java index f16a64f6e5..3d165c856f 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java index 35c818bb81..fbfe069704 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +19,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisURI; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.TimeoutOptions; import io.lettuce.core.resource.ClientResources; @@ -34,17 +35,18 @@ * @author Mark Paluch * @author Christoph Strobl * @author Yanming Zhou + * @author Zhian Chen */ class LettuceClientConfigurationUnitTests { - @Test // DATAREDIS-574, DATAREDIS-576, DATAREDIS-667, DATAREDIS-918 + @Test // DATAREDIS-574, DATAREDIS-576, DATAREDIS-667, DATAREDIS-918, GH-2945 void shouldCreateEmptyConfiguration() { LettuceClientConfiguration configuration = LettuceClientConfiguration.defaultConfiguration(); - assertThat(configuration.isUseSsl()).isFalse(); assertThat(configuration.isVerifyPeer()).isTrue(); + assertThat(configuration.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(configuration.isStartTls()).isFalse(); assertThat(configuration.getClientOptions()).hasValueSatisfying(actual -> { @@ -55,7 +57,7 @@ void shouldCreateEmptyConfiguration() { assertThat(configuration.getClientName()).isEmpty(); assertThat(configuration.getCommandTimeout()).isEqualTo(Duration.ofSeconds(60)); assertThat(configuration.getShutdownTimeout()).isEqualTo(Duration.ofMillis(100)); - assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ofMillis(100)); + assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ZERO); } @Test // DATAREDIS-574, DATAREDIS-576, DATAREDIS-667 @@ -78,6 +80,7 @@ void shouldConfigureAllProperties() { assertThat(configuration.isUseSsl()).isTrue(); assertThat(configuration.isVerifyPeer()).isFalse(); + assertThat(configuration.getVerifyMode().equals(SslVerifyMode.NONE)); assertThat(configuration.isStartTls()).isTrue(); assertThat(configuration.getClientOptions()).contains(clientOptions); assertThat(configuration.getClientResources()).contains(sharedClientResources); @@ -88,13 +91,13 @@ void shouldConfigureAllProperties() { } @Test // DATAREDIS-881 - void shutdownQuietPeriodShouldDefaultToTimeout() { + void shutdownQuietPeriodShouldDefaultInitialValue() { LettuceClientConfiguration configuration = LettuceClientConfiguration.builder() .shutdownTimeout(Duration.ofSeconds(42)).build(); assertThat(configuration.getShutdownTimeout()).isEqualTo(Duration.ofSeconds(42)); - assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ofSeconds(42)); + assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ZERO); } @Test // DATAREDIS-576 @@ -115,6 +118,7 @@ void shouldApplySettingsFromRedisURI() { assertThat(configuration.isUseSsl()).isTrue(); assertThat(configuration.isVerifyPeer()).isTrue(); + assertThat(configuration.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(configuration.isStartTls()).isFalse(); assertThat(configuration.getClientName()).contains("bar"); assertThat(configuration.getCommandTimeout()).isEqualTo(Duration.ofSeconds(10)); diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index 5ecba15478..5590b18275 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,6 +16,7 @@ package org.springframework.data.redis.connection.lettuce; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.data.Offset.*; import static org.assertj.core.data.Offset.offset; import static org.springframework.data.redis.connection.BitFieldSubCommands.*; import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*; @@ -32,6 +33,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; import java.util.*; import java.util.concurrent.TimeUnit; @@ -41,6 +43,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range.Bound; @@ -73,6 +76,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Dennis Neufeld + * @author Tihomir Mateev */ @SuppressWarnings("deprecation") @EnabledOnRedisClusterAvailable @@ -497,8 +501,24 @@ public void expireAtShouldBeSetCorrectly() { assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void expireAtWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000, + ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000, + ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 10000, + ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 - public void expireShouldBeSetCorreclty() { + public void expireShouldBeSetCorrectly() { nativeConnection.set(KEY_1, VALUE_1); @@ -507,6 +527,19 @@ public void expireShouldBeSetCorreclty() { assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void expireWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.expire(KEY_1_BYTES, 15, ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 public void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb() { @@ -1095,6 +1128,182 @@ public void hStrLenReturnsZeroWhenKeyDoesNotExist() { assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_1_BYTES)).isEqualTo(0L); } + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hTtl(KEY_1_BYTES, KEY_2_BYTES)).allSatisfy(val -> assertThat(val).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + // missing field + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hExpire(KEY_2_BYTES, 5L, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5000L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isBetween(0L, 5000L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + // missing field + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hpExpire(KEY_2_BYTES, 5L, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAtReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hTtl(KEY_1_BYTES, KEY_2_BYTES)).allSatisfy(val -> assertThat(val).isBetween(0L, 5L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond(); + + // missing field + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_1_BYTES)).contains(-2L); + + // missing key + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_2_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hExpireAdReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test + @EnabledOnCommand("HEXPIRE") + public void hpExpireAtReturnsSuccessAndSetsTTL() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hpTtl(KEY_1_BYTES, KEY_2_BYTES)) + .allSatisfy(val -> assertThat(val).isGreaterThan(1000L).isLessThanOrEqualTo(5000L)); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli(); + + // missing field + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_1_BYTES)).contains(-2L); + // missing key + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_2_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(-2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hpExpireAdReturnsTwoWhenZeroProvided() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsSuccessAndPersistsField() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(1L); + assertThat(clusterConnection.hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L); + assertThat(clusterConnection.hashCommands().hPersist(KEY_3_BYTES, KEY_2_BYTES)).contains(-2L); + + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, TimeUnit.HOURS, KEY_2_BYTES)).contains(-1L); + } + + @Test // GH-3054 + @EnabledOnCommand("HEXPIRE") + public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() { + + assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L); + assertThat(clusterConnection.hashCommands().hTtl(KEY_3_BYTES, KEY_2_BYTES)).contains(-2L); + } + @Test // DATAREDIS-315 public void hValsShouldRetrieveValuesCorrectly() { @@ -1449,8 +1658,27 @@ public void pExpireAtShouldBeSetCorrectly() { assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void pExpireAtWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000, ExpirationOptions.Condition.XX)) + .isFalse(); + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000, ExpirationOptions.Condition.NX)) + .isTrue(); + assertThat( + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 15000, ExpirationOptions.Condition.LT)) + .isFalse(); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 - public void pExpireShouldBeSetCorreclty() { + public void pExpireShouldBeSetCorrectly() { nativeConnection.set(KEY_1, VALUE_1); @@ -1459,6 +1687,19 @@ public void pExpireShouldBeSetCorreclty() { assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); } + @Test // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + public void pExpireWithConditionShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.XX)).isFalse(); + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.NX)).isTrue(); + assertThat(clusterConnection.pExpire(KEY_1_BYTES, 15000, ExpirationOptions.Condition.LT)).isFalse(); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES))).isGreaterThan(1); + } + @Test // DATAREDIS-315 public void pSetExShouldSetValueCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java index 25fd46d783..7d2a1003bf 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceClusterKeyspaceNotificationsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyspaceNotificationsTests.java index d347283a8b..137423dd57 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyspaceNotificationsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyspaceNotificationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java index 94cacb157f..c3d3997514 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java index 3c91253f23..0c2d1d65e2 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -402,7 +402,7 @@ void connectsThroughRedisSocket() { @Test // DATAREDIS-762, DATAREDIS-869 void factoryUsesElastiCacheMasterReplicaConnections() { - assumeTrue(String.format("No replicas connected to %s:%s.", SettingsUtils.getHost(), SettingsUtils.getPort()), + assumeTrue("No replicas connected to %s:%d".formatted(SettingsUtils.getHost(), SettingsUtils.getPort()), connection.info("replication").getProperty("connected_slaves", "0").compareTo("0") > 0); LettuceClientConfiguration configuration = LettuceTestClientConfiguration.builder().readFrom(ReadFrom.REPLICA) @@ -429,7 +429,7 @@ void factoryUsesElastiCacheMasterReplicaConnections() { @Test // DATAREDIS-1093 void pubSubDoesNotSupportMasterReplicaConnections() { - assumeTrue(String.format("No replicas connected to %s:%s.", SettingsUtils.getHost(), SettingsUtils.getPort()), + assumeTrue("No replicas connected to %s:%d".formatted(SettingsUtils.getHost(), SettingsUtils.getPort()), connection.info("replication").getProperty("connected_slaves", "0").compareTo("0") > 0); RedisStaticMasterReplicaConfiguration elastiCache = new RedisStaticMasterReplicaConfiguration( @@ -451,7 +451,7 @@ void pubSubDoesNotSupportMasterReplicaConnections() { @Test // DATAREDIS-762, DATAREDIS-869 void factoryUsesElastiCacheMasterWithoutMaster() { - assumeTrue(String.format("No replicas connected to %s:%s.", SettingsUtils.getHost(), SettingsUtils.getPort()), + assumeTrue("No replicas connected to %s:%d.".formatted(SettingsUtils.getHost(), SettingsUtils.getPort()), connection.info("replication").getProperty("connected_slaves", "0").compareTo("0") > 0); LettuceClientConfiguration configuration = LettuceTestClientConfiguration.builder().readFrom(ReadFrom.MASTER) @@ -481,7 +481,7 @@ void factoryUsesElastiCacheMasterWithoutMaster() { @Test // DATAREDIS-580, DATAREDIS-869 void factoryUsesMasterReplicaConnections() { - assumeTrue(String.format("No replicas connected to %s:%s.", SettingsUtils.getHost(), SettingsUtils.getPort()), + assumeTrue("No replicas connected to %s:%d".formatted(SettingsUtils.getHost(), SettingsUtils.getPort()), connection.info("replication").getProperty("connected_slaves", "0").compareTo("0") > 0); LettuceClientConfiguration configuration = LettuceTestClientConfiguration.builder().readFrom(ReadFrom.SLAVE) @@ -489,6 +489,7 @@ void factoryUsesMasterReplicaConnections() { LettuceConnectionFactory factory = new LettuceConnectionFactory(SettingsUtils.standaloneConfiguration(), configuration); + factory.start(); RedisConnection connection = factory.getConnection(); diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java index b4dd713fbe..8fd3f0c747 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,6 +26,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.ClusterClientOptions; @@ -76,6 +77,7 @@ * @author Andrea Como * @author Chris Bono * @author John Blum + * @author Zhian Chen */ class LettuceConnectionFactoryUnitTests { @@ -374,7 +376,9 @@ void sslOptionsShouldBeDisabledByDefaultOnClient() { assertThat(redisUri.isStartTls()).isFalse(); assertThat(connectionFactory.isStartTls()).isFalse(); assertThat(redisUri.isVerifyPeer()).isTrue(); + assertThat(redisUri.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(connectionFactory.isVerifyPeer()).isTrue(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.FULL)); } @Test // DATAREDIS-476 @@ -393,7 +397,9 @@ void sslShouldBeSetCorrectlyOnClient() { assertThat(redisUri.isSsl()).isTrue(); assertThat(connectionFactory.isUseSsl()).isTrue(); assertThat(redisUri.isVerifyPeer()).isTrue(); + assertThat(redisUri.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(connectionFactory.isVerifyPeer()).isTrue(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.FULL)); } @Test // DATAREDIS-480 @@ -411,7 +417,9 @@ void verifyPeerOptionShouldBeSetCorrectlyOnClient() { RedisURI redisUri = (RedisURI) getField(client, "redisURI"); assertThat(redisUri.isVerifyPeer()).isFalse(); + assertThat(redisUri.getVerifyMode().equals(SslVerifyMode.NONE)); assertThat(connectionFactory.isVerifyPeer()).isFalse(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.NONE)); } @Test // DATAREDIS-480 @@ -450,7 +458,9 @@ void sslShouldBeSetCorrectlyOnSentinelClient() { assertThat(redisUri.isSsl()).isTrue(); assertThat(connectionFactory.isUseSsl()).isTrue(); assertThat(redisUri.isVerifyPeer()).isTrue(); + assertThat(redisUri.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(connectionFactory.isVerifyPeer()).isTrue(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.FULL)); } @Test // DATAREDIS-990 @@ -470,6 +480,7 @@ void verifyPeerOptionShouldBeSetCorrectlyOnSentinelClient() { assertThat(redisUri.isVerifyPeer()).isFalse(); assertThat(connectionFactory.isVerifyPeer()).isFalse(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.NONE)); } @Test // DATAREDIS-990 @@ -545,6 +556,7 @@ void verifyPeerTLSOptionShouldBeSetCorrectlyOnClusterClient() { for (RedisURI uri : initialUris) { assertThat(uri.isVerifyPeer()).isTrue(); + assertThat(uri.getVerifyMode().equals(SslVerifyMode.FULL)); } } @@ -745,6 +757,7 @@ void shouldApplyClientConfiguration() { assertThat(connectionFactory.isUseSsl()).isTrue(); assertThat(connectionFactory.isVerifyPeer()).isFalse(); + assertThat(connectionFactory.getClientConfiguration().getVerifyMode().equals(SslVerifyMode.NONE)); assertThat(connectionFactory.isStartTls()).isTrue(); assertThat(connectionFactory.getClientResources()).isEqualTo(sharedClientResources); assertThat(connectionFactory.getTimeout()).isEqualTo(Duration.ofMinutes(5).toMillis()); @@ -1058,7 +1071,6 @@ void maxRedirectsShouldBeSetOnClusterClientOptions() { assertThat(options.getMaxRedirects()).isEqualTo(42); assertThat(options.isValidateClusterNodeMembership()).isFalse(); - assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isFalse(); } @Test // DATAREDIS-1142 diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java index 7ca1190049..afe2108296 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineFlushOnEndIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineFlushOnEndIntegrationTests.java index 1e4133d154..c810a396a1 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineFlushOnEndIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineFlushOnEndIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java index c9e702d2c0..9793364915 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxFlushOnEndIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxFlushOnEndIntegrationTests.java index e5e49d6a8b..fa4004078a 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxFlushOnEndIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxFlushOnEndIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java index 0ad92314da..2d965cbfdf 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java index 0b6f1653ef..f5e153c8dc 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionUnitTests.java index 0a6c19a166..0ba0123eac 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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. @@ -15,12 +15,33 @@ */ package org.springframework.data.redis.connection.lettuce; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import io.lettuce.core.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.lettuce.core.KeyScanCursor; +import io.lettuce.core.MapScanCursor; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; +import io.lettuce.core.ScoredValue; +import io.lettuce.core.ScoredValueScanCursor; +import io.lettuce.core.ValueScanCursor; +import io.lettuce.core.XAddArgs; +import io.lettuce.core.XClaimArgs; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.codec.ByteArrayCodec; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; @@ -41,6 +62,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -59,40 +81,44 @@ * @author Christoph Strobl * @author Mark Paluch */ -public class LettuceConnectionUnitTests { +class LettuceConnectionUnitTests { - @SuppressWarnings("rawtypes") - public static class BasicUnitTests extends AbstractConnectionUnitTestBase { + protected LettuceConnection connection; + private RedisClient clientMock; + StatefulRedisConnection statefulConnectionMock; + RedisAsyncCommands asyncCommandsMock; + RedisCommands commandsMock; - protected LettuceConnection connection; - private RedisClient clientMock; - StatefulRedisConnection statefulConnectionMock; - RedisAsyncCommands asyncCommandsMock; + @BeforeEach + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void setUp() throws InvocationTargetException, IllegalAccessException { - @SuppressWarnings({ "unchecked" }) - @BeforeEach - public void setUp() throws InvocationTargetException, IllegalAccessException { + clientMock = mock(RedisClient.class); + statefulConnectionMock = mock(StatefulRedisConnection.class); + when(clientMock.connect((RedisCodec) any())).thenReturn(statefulConnectionMock); - clientMock = mock(RedisClient.class); - statefulConnectionMock = mock(StatefulRedisConnection.class); - when(clientMock.connect((RedisCodec) any())).thenReturn(statefulConnectionMock); + asyncCommandsMock = Mockito.mock(RedisAsyncCommands.class, invocation -> { - asyncCommandsMock = Mockito.mock(RedisAsyncCommands.class, invocation -> { + if (invocation.getMethod().getReturnType().equals(RedisFuture.class)) { - if (invocation.getMethod().getReturnType().equals(RedisFuture.class)) { + Command cmd = new Command<>(CommandType.PING, new StatusOutput<>(StringCodec.UTF8)); + AsyncCommand async = new AsyncCommand<>(cmd); + async.complete(); - Command cmd = new Command<>(CommandType.PING, new StatusOutput<>(StringCodec.UTF8)); - AsyncCommand async = new AsyncCommand<>(cmd); - async.complete(); + return async; + } + return null; + }); + commandsMock = Mockito.mock(RedisCommands.class); - return async; - } - return null; - }); + when(statefulConnectionMock.async()).thenReturn(asyncCommandsMock); + when(statefulConnectionMock.sync()).thenReturn(commandsMock); + connection = new LettuceConnection(0, clientMock); + } - when(statefulConnectionMock.async()).thenReturn(asyncCommandsMock); - connection = new LettuceConnection(0, clientMock); - } + @Nested + @SuppressWarnings({ "rawtypes", "deprecation" }) + class BasicUnitTests extends AbstractConnectionUnitTestBase { @Test // DATAREDIS-184 public void shutdownWithNullOptionsIsCalledCorrectly() { @@ -155,7 +181,7 @@ void shouldThrowExceptionWhenAccessingRedisSentinelsCommandsWhenNoSentinelsConfi .isThrownBy(() -> connection.getSentinelConnection()); } - @Test // DATAREDIS-431 + @Test // DATAREDIS-431, GH-2984 void dbIndexShouldBeSetWhenObtainingConnection() { connection = new LettuceConnection(null, 0, clientMock, 0); @@ -163,6 +189,7 @@ void dbIndexShouldBeSetWhenObtainingConnection() { connection.getNativeConnection(); verify(asyncCommandsMock).dispatch(eq(CommandType.SELECT), any(), any()); + verifyNoInteractions(commandsMock); } @Test // DATAREDIS-603 @@ -173,8 +200,7 @@ void translatesUnknownExceptions() { when(asyncCommandsMock.set(any(), any())).thenThrow(exception); connection = new LettuceConnection(null, 0, clientMock, 1); - assertThatThrownBy(() -> connection.set("foo".getBytes(), "bar".getBytes())) - .hasRootCause(exception); + assertThatThrownBy(() -> connection.set("foo".getBytes(), "bar".getBytes())).hasRootCause(exception); } @Test // DATAREDIS-603 @@ -265,8 +291,8 @@ public List getKeys() { sc.setCursor(cursorId); sc.setFinished(false); - Command> command = new Command<>(new LettuceConnection.CustomCommandType("SCAN"), - new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { + Command> command = new Command<>( + new LettuceConnection.CustomCommandType("SCAN"), new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { @Override protected void setOutput(ByteBuffer bytes) { @@ -275,10 +301,10 @@ protected void setOutput(ByteBuffer bytes) { AsyncCommand> future = new AsyncCommand<>(command); future.complete(); - when(asyncCommandsMock.scan(any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future); + when(asyncCommandsMock.scan(any(ScanCursor.class), any(ScanArgs.class))).thenReturn(future, future); Cursor cursor = connection.scan(KeyScanOptions.NONE); - cursor.next(); //initial + cursor.next(); // initial assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId)); cursor.next(); // fetch next @@ -300,8 +326,8 @@ public List getValues() { sc.setCursor(cursorId); sc.setFinished(false); - Command> command = new Command<>(new LettuceConnection.CustomCommandType("SSCAN"), - new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { + Command> command = new Command<>( + new LettuceConnection.CustomCommandType("SSCAN"), new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { @Override protected void setOutput(ByteBuffer bytes) { @@ -310,10 +336,11 @@ protected void setOutput(ByteBuffer bytes) { AsyncCommand> future = new AsyncCommand<>(command); future.complete(); - when(asyncCommandsMock.sscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future); + when(asyncCommandsMock.sscan(any(byte[].class), any(ScanCursor.class), any(ScanArgs.class))).thenReturn(future, + future); Cursor cursor = connection.setCommands().sScan("key".getBytes(), KeyScanOptions.NONE); - cursor.next(); //initial + cursor.next(); // initial assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId)); cursor.next(); // fetch next @@ -335,8 +362,8 @@ public List> getValues() { sc.setCursor(cursorId); sc.setFinished(false); - Command> command = new Command<>(new LettuceConnection.CustomCommandType("ZSCAN"), - new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { + Command> command = new Command<>( + new LettuceConnection.CustomCommandType("ZSCAN"), new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { @Override protected void setOutput(ByteBuffer bytes) { @@ -345,10 +372,11 @@ protected void setOutput(ByteBuffer bytes) { AsyncCommand> future = new AsyncCommand<>(command); future.complete(); - when(asyncCommandsMock.zscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future); + when(asyncCommandsMock.zscan(any(byte[].class), any(ScanCursor.class), any(ScanArgs.class))).thenReturn(future, + future); Cursor cursor = connection.zSetCommands().zScan("key".getBytes(), KeyScanOptions.NONE); - cursor.next(); //initial + cursor.next(); // initial assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId)); cursor.next(); // fetch next @@ -370,8 +398,8 @@ public Map getMap() { sc.setCursor(cursorId); sc.setFinished(false); - Command> command = new Command<>(new LettuceConnection.CustomCommandType("HSCAN"), - new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { + Command> command = new Command<>( + new LettuceConnection.CustomCommandType("HSCAN"), new ScanOutput<>(ByteArrayCodec.INSTANCE, sc) { @Override protected void setOutput(ByteBuffer bytes) { @@ -380,10 +408,11 @@ protected void setOutput(ByteBuffer bytes) { AsyncCommand> future = new AsyncCommand<>(command); future.complete(); - when(asyncCommandsMock.hscan(any(byte[].class), any(ScanCursor.class),any(ScanArgs.class))).thenReturn(future, future); + when(asyncCommandsMock.hscan(any(byte[].class), any(ScanCursor.class), any(ScanArgs.class))).thenReturn(future, + future); Cursor> cursor = connection.hashCommands().hScan("key".getBytes(), KeyScanOptions.NONE); - cursor.next(); //initial + cursor.next(); // initial assertThat(cursor.getCursorId()).isEqualTo(Long.parseUnsignedLong(cursorId)); cursor.next(); // fetch next @@ -394,13 +423,12 @@ protected void setOutput(ByteBuffer bytes) { } - public static class LettucePipelineConnectionUnitTests extends BasicUnitTests { + @Nested + class LettucePipelineConnectionUnitTests extends BasicUnitTests { - @Override @BeforeEach public void setUp() throws InvocationTargetException, IllegalAccessException { - super.setUp(); - this.connection.openPipeline(); + connection.openPipeline(); } @Test // DATAREDIS-528 diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 0e4c15fffb..74cb969dc6 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfigurationUnitTests.java index 85215098ff..55f06d19b2 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,6 +19,7 @@ import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; +import io.lettuce.core.SslVerifyMode; import io.lettuce.core.TimeoutOptions; import io.lettuce.core.resource.ClientResources; @@ -35,10 +36,11 @@ * @author Mark Paluch * @author Christoph Strobl * @author Longlong Zhao + * @author Zhian Chen */ class LettucePoolingClientConfigurationUnitTests { - @Test // DATAREDIS-667, DATAREDIS-918 + @Test // DATAREDIS-667, DATAREDIS-918, GH-2945 void shouldCreateEmptyConfiguration() { LettucePoolingClientConfiguration configuration = LettucePoolingClientConfiguration.defaultConfiguration(); @@ -46,6 +48,7 @@ void shouldCreateEmptyConfiguration() { assertThat(configuration.getPoolConfig()).isNotNull(); assertThat(configuration.isUseSsl()).isFalse(); assertThat(configuration.isVerifyPeer()).isTrue(); + assertThat(configuration.getVerifyMode().equals(SslVerifyMode.FULL)); assertThat(configuration.isStartTls()).isFalse(); assertThat(configuration.getClientOptions()).hasValueSatisfying(actual -> { @@ -55,7 +58,7 @@ void shouldCreateEmptyConfiguration() { assertThat(configuration.getClientResources()).isEmpty(); assertThat(configuration.getCommandTimeout()).isEqualTo(Duration.ofSeconds(60)); assertThat(configuration.getShutdownTimeout()).isEqualTo(Duration.ofMillis(100)); - assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ofMillis(100)); + assertThat(configuration.getShutdownQuietPeriod()).isEqualTo(Duration.ZERO); } @Test // DATAREDIS-667 @@ -80,6 +83,7 @@ void shouldConfigureAllProperties() { assertThat(configuration.getPoolConfig()).isEqualTo(poolConfig); assertThat(configuration.isUseSsl()).isTrue(); assertThat(configuration.isVerifyPeer()).isFalse(); + assertThat(configuration.getVerifyMode().equals(SslVerifyMode.NONE)); assertThat(configuration.isStartTls()).isTrue(); assertThat(configuration.getClientOptions()).contains(clientOptions); assertThat(configuration.getClientResources()).contains(sharedClientResources); diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProviderUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProviderUnitTests.java index ebd84f545e..4c4cfe735a 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProviderUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProviderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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. @@ -17,9 +17,11 @@ import static org.mockito.Mockito.*; +import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,11 +29,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder; /** * Unit tests for {@link LettucePoolingConnectionProvider}. * * @author Mark Paluch + * @author Asmir Mustafic */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -70,4 +74,21 @@ void shouldDiscardTransactionOnReleaseOnActiveTransaction() { verify(commandsMock).discard(); } + + @Test // GH-3072 + void shouldPrepareThePool() { + + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMinIdle(5); + poolConfig.setMaxIdle(8); + poolConfig.setMaxTotal(10); + + LettucePoolingClientConfiguration config = new LettucePoolingClientConfigurationBuilder().poolConfig(poolConfig) + .build(); + + LettucePoolingConnectionProvider provider = new LettucePoolingConnectionProvider(connectionProviderMock, config); + + provider.getConnection(StatefulRedisConnection.class); + verify(connectionProviderMock, times(5)).getConnection(any()); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsIntegrationTests.java index 5c439889b6..d069de8231 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsIntegrationTests.java index 3d94e4a394..70c1932b3e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsIntegrationTests.java index 4d08c1402d..aa7c7fee12 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterListCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsIntegrationTests.java index 6632a1f97c..f2afb32b4e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterServerCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommandsIntegrationTests.java index 197a2b53a1..bb692e97fe 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterServerCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveClusterStringCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsIntegrationTests.java index 13848ee84c..1f56137cf2 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveClusterTestSupport.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterTestSupport.java index d253e49cf9..3e900e339f 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterTestSupport.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,12 +52,12 @@ public void tearDown() { if (nativeCommands != null) { nativeCommands.flushall(); - if (nativeCommands instanceof RedisCommands) { - ((RedisCommands) nativeCommands).getStatefulConnection().close(); + if (nativeCommands instanceof RedisCommands redisCommands) { + redisCommands.getStatefulConnection().close(); } - if (nativeCommands instanceof RedisAdvancedClusterCommands) { - ((RedisAdvancedClusterCommands) nativeCommands).getStatefulConnection().close(); + if (nativeCommands instanceof RedisAdvancedClusterCommands redisAdvancedClusterCommands) { + redisAdvancedClusterCommands.getStatefulConnection().close(); } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsIntegrationTests.java index 0bedf8273f..17684f0dce 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveCommandsTestSupport.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestSupport.java index d06df9f15b..3407fea953 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestSupport.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,16 +155,16 @@ public String toString() { public void close() throws IOException { try { - if (connectionProvider instanceof DisposableBean) { - ((DisposableBean) connectionProvider).destroy(); + if (connectionProvider instanceof DisposableBean disposableBean) { + disposableBean.destroy(); } - if (nativeConnectionProvider instanceof DisposableBean) { - ((DisposableBean) nativeConnectionProvider).destroy(); + if (nativeConnectionProvider instanceof DisposableBean disposableBean) { + disposableBean.destroy(); } - if (nativeBinaryConnectionProvider instanceof DisposableBean) { - ((DisposableBean) nativeBinaryConnectionProvider).destroy(); + if (nativeBinaryConnectionProvider instanceof DisposableBean disposableBean) { + disposableBean.destroy(); } } catch (Exception ex) { throw new RuntimeException(ex); @@ -198,21 +198,21 @@ public void tearDown() { if (nativeCommands != null) { flushAll(); - if (nativeCommands instanceof RedisCommands) { - nativeConnectionProvider.release(((RedisCommands) nativeCommands).getStatefulConnection()); + if (nativeCommands instanceof RedisCommands redisCommands) { + nativeConnectionProvider.release((redisCommands).getStatefulConnection()); } - if (nativeCommands instanceof RedisAdvancedClusterCommands) { - nativeConnectionProvider.release(((RedisAdvancedClusterCommands) nativeCommands).getStatefulConnection()); + if (nativeCommands instanceof RedisAdvancedClusterCommands redisAdvancedClusterCommands) { + nativeConnectionProvider.release((redisAdvancedClusterCommands).getStatefulConnection()); } - if (nativeBinaryCommands instanceof RedisCommands) { - nativeBinaryConnectionProvider.release(((RedisCommands) nativeBinaryCommands).getStatefulConnection()); + if (nativeBinaryCommands instanceof RedisCommands redisCommands) { + nativeBinaryConnectionProvider.release((redisCommands).getStatefulConnection()); } - if (nativeBinaryCommands instanceof RedisAdvancedClusterCommands) { + if (nativeBinaryCommands instanceof RedisAdvancedClusterCommands redisAdvancedClusterCommands) { nativeBinaryConnectionProvider - .release(((RedisAdvancedClusterCommands) nativeBinaryCommands).getStatefulConnection()); + .release((redisAdvancedClusterCommands).getStatefulConnection()); } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java index f01cd12658..5f3d43e2b8 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveHashCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsIntegrationTests.java index e152cf6311..bc1f8cc204 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,12 +15,14 @@ */ package org.springframework.data.redis.connection.lettuce; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import reactor.test.StepVerifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -28,6 +30,7 @@ import java.util.Map; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; /** @@ -35,6 +38,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Tihomir Mateev */ public class LettuceReactiveHashCommandsIntegrationTests extends LettuceReactiveCommandsTestSupport { @@ -103,8 +107,7 @@ void hMGetShouldReturnValueForFields() { nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); connection.hashCommands().hMGet(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_3_BBUFFER)) - .as(StepVerifier::create) - .consumeNextWith(actual -> { + .as(StepVerifier::create).consumeNextWith(actual -> { assertThat(actual).contains(VALUE_1_BBUFFER, VALUE_3_BBUFFER); @@ -120,13 +123,11 @@ void hMGetShouldReturnNullValueForFieldsThatHaveNoValue() { connection.hashCommands().hMGet(KEY_1_BBUFFER, Collections.singletonList(FIELD_1_BBUFFER)).as(StepVerifier::create) .expectNext(Collections.singletonList(VALUE_1_BBUFFER)).verifyComplete(); - connection.hashCommands().hMGet(KEY_1_BBUFFER, Collections.singletonList(FIELD_2_BBUFFER)) - .as(StepVerifier::create) + connection.hashCommands().hMGet(KEY_1_BBUFFER, Collections.singletonList(FIELD_2_BBUFFER)).as(StepVerifier::create) .expectNext(Collections.singletonList(null)).verifyComplete(); connection.hashCommands().hMGet(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER)) - .as(StepVerifier::create) - .expectNext(Arrays.asList(VALUE_1_BBUFFER, null, VALUE_3_BBUFFER)).verifyComplete(); + .as(StepVerifier::create).expectNext(Arrays.asList(VALUE_1_BBUFFER, null, VALUE_3_BBUFFER)).verifyComplete(); } @ParameterizedRedisTest // DATAREDIS-525 @@ -193,8 +194,7 @@ void hDelShouldRemoveMultipleFieldsCorrectly() { nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); connection.hashCommands().hDel(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_3_BBUFFER)) - .as(StepVerifier::create) - .expectNext(2L).verifyComplete(); + .as(StepVerifier::create).expectNext(2L).verifyComplete(); } @ParameterizedRedisTest // DATAREDIS-525 @@ -288,4 +288,52 @@ void hStrLenReturnsZeroWhenKeyDoesNotExist() { connection.hashCommands().hStrLen(KEY_1_BBUFFER, FIELD_1_BBUFFER).as(StepVerifier::create).expectNext(0L) // .verifyComplete(); } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void hExpireShouldHandleMultipleParametersCorrectly() { + + assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue(); + assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue(); + final var fields = Arrays.asList(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER); + + connection.hashCommands().hExpire(KEY_1_BBUFFER, Duration.ofSeconds(1), fields).as(StepVerifier::create) // + .expectNext(1L).expectNext(1L).expectNext(-2L).expectComplete().verify(); + + assertThat(nativeCommands.httl(KEY_1, FIELD_1)).allSatisfy(it -> assertThat(it).isBetween(0L, 1000L)); + assertThat(nativeCommands.httl(KEY_1, FIELD_2)).allSatisfy(it -> assertThat(it).isBetween(0L, 1000L)); + assertThat(nativeCommands.httl(KEY_1, FIELD_3)).allSatisfy(it -> assertThat(it).isEqualTo(-2L)); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void hExpireAtShouldHandleMultipleParametersCorrectly() { + + assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue(); + assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue(); + final var fields = Arrays.asList(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER); + + connection.hashCommands().hExpireAt(KEY_1_BBUFFER, Instant.now().plusSeconds(1), fields).as(StepVerifier::create) // + .expectNext(1L).expectNext(1L).expectNext(-2L).expectComplete().verify(); + + assertThat(nativeCommands.httl(KEY_1, FIELD_1, FIELD_2)).allSatisfy(it -> assertThat(it).isBetween(0L, 1000L)); + assertThat(nativeCommands.httl(KEY_1, FIELD_3)).allSatisfy(it -> assertThat(it).isEqualTo(-2L)); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void hPersistShouldPersistFields() { + + assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue(); + assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue(); + + assertThat(nativeCommands.hexpire(KEY_1, 1000, FIELD_1)).allSatisfy(it -> assertThat(it).isEqualTo(1L)); + + final var fields = Arrays.asList(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER); + + connection.hashCommands().hPersist(KEY_1_BBUFFER, fields).as(StepVerifier::create) // + .expectNext(1L).expectNext(-1L).expectNext(-2L).expectComplete().verify(); + + assertThat(nativeCommands.httl(KEY_1, FIELD_1, FIELD_2)).allSatisfy(it -> assertThat(it).isEqualTo(-1L)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java index cdd9e99d8c..2b33e87a38 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java index 457c009a60..5dd4f5684b 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,11 +32,15 @@ import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; +import org.springframework.data.redis.connection.ReactiveKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding; import org.springframework.data.redis.core.KeyScanOptions; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.condition.EnabledOnRedisVersion; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -46,6 +50,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Dahye Anne Lee */ public class LettuceReactiveKeyCommandsIntegrationTests extends LettuceReactiveCommandsTestSupport { @@ -66,6 +71,23 @@ void existsShouldReturnFalseForNonExistingKeys() { connection.keyCommands().exists(KEY_1_BBUFFER).as(StepVerifier::create).expectNext(false).verifyComplete(); } + @ParameterizedRedisTest // GH-2883 + void existsKeyReturnsKeyCount() { + + nativeCommands.set(KEY_1, "1000"); + nativeCommands.set(KEY_2, "2000"); + nativeCommands.set(KEY_3, "3000"); + + connection.keyCommands().exists(List.of(KEY_1_BBUFFER, KEY_2_BBUFFER, KEY_3_BBUFFER)).as(StepVerifier::create) + .expectNext(3L).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2883 + void existsKeyReturnsZeroWhenKeysDoNotExist() { + connection.keyCommands().exists(List.of(KEY_1_BBUFFER, KEY_2_BBUFFER, KEY_3_BBUFFER)).as(StepVerifier::create) + .expectNext(0L).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-525 void typeShouldReturnTypeCorrectly() { @@ -164,7 +186,7 @@ void renameShouldAlterKeyNameCorrectly() { connection.keyCommands().rename(KEY_1_BBUFFER, KEY_2_BBUFFER).as(StepVerifier::create).expectNext(true) .verifyComplete(); assertThat(nativeCommands.exists(KEY_2)).isEqualTo(1L); - assertThat(nativeCommands.exists(KEY_1)).isEqualTo(0L); + assertThat(nativeCommands.exists(KEY_1)).isZero(); } @ParameterizedRedisTest // DATAREDIS-525 @@ -183,7 +205,7 @@ void renameNXShouldAlterKeyNameCorrectly() { .verifyComplete(); assertThat(nativeCommands.exists(KEY_2)).isEqualTo(1L); - assertThat(nativeCommands.exists(KEY_1)).isEqualTo(0L); + assertThat(nativeCommands.exists(KEY_1)).isZero(); } @ParameterizedRedisTest // DATAREDIS-525 @@ -303,6 +325,35 @@ void shouldExpireKeysCorrectly() { assertThat(nativeCommands.ttl(KEY_1)).isGreaterThan(8L); } + @ParameterizedRedisTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void shouldExpireWithOptionsKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.keyCommands() + .applyExpiration( + Mono.just(ReactiveKeyCommands.ExpireCommand.expire(KEY_1_BBUFFER, Expiration.from(Duration.ofSeconds(10))) + .withOptions(ExpirationOptions.builder().xx().build()))) + .map(ReactiveRedisConnection.BooleanResponse::getOutput).as(StepVerifier::create) // + .expectNext(false) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1)).isEqualTo(-1L); + + connection.keyCommands() + .applyExpiration( + Mono.just(ReactiveKeyCommands.ExpireCommand.expire(KEY_1_BBUFFER, Expiration.from(Duration.ofSeconds(10))) + .withOptions(ExpirationOptions.builder().nx().build()))) + .map(ReactiveRedisConnection.BooleanResponse::getOutput).as(StepVerifier::create) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1)).isGreaterThan(8L); + } + @ParameterizedRedisTest // DATAREDIS-602, DATAREDIS-1031 void shouldPreciseExpireKeysCorrectly() { @@ -395,7 +446,7 @@ void shouldMoveToDatabase() { .expectNext(true) // .expectComplete() // .verify(); - assertThat(nativeCommands.exists(KEY_1)).isEqualTo(0L); + assertThat(nativeCommands.exists(KEY_1)).isZero(); } @ParameterizedRedisTest // DATAREDIS-694 diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java index 856466cfc0..b09aa7fe5c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveNumberCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsIntegrationTests.java index cf072ff594..0b80066cfc 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactivePubSubCommandsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommandsUnitTests.java index c4013494ce..17d976e7ac 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommandsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactivePubSubCommandsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/lettuce/LettuceReactiveRedisClusterConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnectionUnitTests.java index 4c4b6b7f7e..af050574b9 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnectionUnitTests.java index e8fe23e141..7e56c13cdb 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveScriptingCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommandsIntegrationTests.java index 75a0b75a3a..17114f4405 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -111,7 +111,7 @@ void evalShaShouldFail() { @ParameterizedRedisTest // DATAREDIS-683 void evalShouldReturnStatus() { - ByteBuffer script = wrap(String.format("return redis.call('set','%s','ghk')", SAME_SLOT_KEY_1)); + ByteBuffer script = wrap("return redis.call('set','%s','ghk')".formatted(SAME_SLOT_KEY_1)); connection.scriptingCommands().eval(script, ReturnType.STATUS, 1, SAME_SLOT_KEY_1_BBUFFER.duplicate()) .as(StepVerifier::create) // diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommandsIntegrationTests.java index 09be137a6d..ce6a8ebefe 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveServerCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/LettuceReactiveSetCommandsIntegrationIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsIntegrationIntegrationTests.java index 394cb5cba9..f8046aef2c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsIntegrationIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsIntegrationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceReactiveStreamCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsIntegrationTests.java index 9fb4d8401b..b181ef6a60 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java index 15548cfc22..530ec229b9 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -556,4 +556,32 @@ void setKeepTTL() { assertThat(nativeBinaryCommands.ttl(KEY_1_BBUFFER)).isCloseTo(expireSeconds, Offset.offset(5L)); assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); } + + @ParameterizedRedisTest // GH-2853 + void setGetMono() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().setGet(KEY_1_BBUFFER, VALUE_2_BBUFFER, Expiration.keepTtl(), SetOption.upsert()) + .as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); + } + + @ParameterizedRedisTest // GH-2853 + void setGetFlux() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().setGet(Mono.just(SetCommand.set(KEY_1_BBUFFER).value(VALUE_2_BBUFFER).expiring(Expiration.keepTtl()).withSetOption( SetOption.upsert()))) + .map(CommandResponse::getOutput) + .as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscriptionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscriptionUnitTests.java index 9583d1f760..61f4daf71a 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscriptionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSubscriptionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsIntegrationTests.java index 78b69f568d..1dac69219b 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceSentinelConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnectionUnitTests.java index 3785f6fdcb..4dd4e31b5b 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelConnectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelIntegrationTests.java index 7c119c3d03..cb57462908 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSentinelIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/connection/lettuce/LettuceSubscriptionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSubscriptionUnitTests.java index 4c1ba0f9ec..bec4c050cb 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSubscriptionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceSubscriptionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/connection/lettuce/LettuceTestClientConfiguration.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceTestClientConfiguration.java index 086ccf86b0..04c295000e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceTestClientConfiguration.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceTestClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/connection/lettuce/PipeliningFlushPolicyUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/PipeliningFlushPolicyUnitTests.java index 00ab6acf52..1dedf516dd 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/PipeliningFlushPolicyUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/PipeliningFlushPolicyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/StaticMasterReplicaConnectionProviderIntegrationTest.java b/src/test/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProviderIntegrationTest.java index c585c5d3ee..07f17de6aa 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProviderIntegrationTest.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/StaticMasterReplicaConnectionProviderIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/connection/lettuce/TransactionalLettuceIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/TransactionalLettuceIntegrationTests.java index 9e5f14a6ce..c3a55b3539 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/TransactionalLettuceIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/TransactionalLettuceIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/connection/lettuce/extension/LettuceConnectionFactoryExtension.java b/src/test/java/org/springframework/data/redis/connection/lettuce/extension/LettuceConnectionFactoryExtension.java index 791c6b312e..e646e12593 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/extension/LettuceConnectionFactoryExtension.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/extension/LettuceConnectionFactoryExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/connection/lettuce/observability/ReactiveIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observability/ReactiveIntegrationTests.java deleted file mode 100644 index cdbaf8661a..0000000000 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/observability/ReactiveIntegrationTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.connection.ReactiveRedisConnection; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; -import io.micrometer.tracing.test.SampleTestRunner; -import reactor.test.StepVerifier; -import reactor.util.context.Context; - -/** - * Collection of tests that log metrics and tracing using the reactive API. - * - * @author Mark Paluch - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = TestConfig.class) -public class ReactiveIntegrationTests extends SampleTestRunner { - - @Autowired LettuceConnectionFactory connectionFactory; - - ReactiveIntegrationTests() { - super(SampleRunnerConfig.builder().build()); - } - - @Override - protected MeterRegistry createMeterRegistry() { - return TestConfig.METER_REGISTRY; - } - - @Override - protected ObservationRegistry createObservationRegistry() { - return TestConfig.OBSERVATION_REGISTRY; - } - - @Override - public SampleTestRunnerConsumer yourCode() { - - return (tracer, meterRegistry) -> { - - Observation intermediate = Observation.start("intermediate", createObservationRegistry()); - - ReactiveRedisConnection connection = connectionFactory.getReactiveConnection(); - - connection.ping().contextWrite(Context.of(ObservationThreadLocalAccessor.KEY, intermediate)) - .as(StepVerifier::create).expectNext("PONG").verifyComplete(); - - intermediate.stop(); - - connection.close(); - - assertThat(tracer.getFinishedSpans()).isNotEmpty(); - System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); - }; - } -} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/observability/SynchronousIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observability/SynchronousIntegrationTests.java deleted file mode 100644 index 52bbd0b03f..0000000000 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/observability/SynchronousIntegrationTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.SettingsUtils; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.tracing.exporter.FinishedSpan; -import io.micrometer.tracing.test.SampleTestRunner; - -/** - * Collection of tests that log metrics and tracing using the synchronous API. - * - * @author Mark Paluch - * @author Yanming Zhou - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = TestConfig.class) -public class SynchronousIntegrationTests extends SampleTestRunner { - - @Autowired LettuceConnectionFactory connectionFactory; - - SynchronousIntegrationTests() { - super(SampleRunnerConfig.builder().build()); - } - - @Override - protected MeterRegistry createMeterRegistry() { - return TestConfig.METER_REGISTRY; - } - - @Override - protected ObservationRegistry createObservationRegistry() { - return TestConfig.OBSERVATION_REGISTRY; - } - - @Override - public SampleTestRunnerConsumer yourCode() { - - return (tracer, meterRegistry) -> { - - RedisConnection connection = connectionFactory.getConnection(); - connection.ping(); - - connection.close(); - - assertThat(tracer.getFinishedSpans()).isNotEmpty(); - System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); - - assertThat(tracer.getFinishedSpans()).isNotEmpty(); - - for (FinishedSpan finishedSpan : tracer.getFinishedSpans()) { - assertThat(finishedSpan.getTags()).containsEntry("db.system", "redis") - .containsEntry("net.sock.peer.addr", SettingsUtils.getHost()) - .containsEntry("net.sock.peer.port", "" + SettingsUtils.getPort()); - assertThat(finishedSpan.getTags()).containsKeys("db.operation"); - } - - assertThat(TestConfig.PARENT_OBSERVATION_NAMES_COLLECTED_IN_PREDICATE).isNotEmpty(); - TestConfig.PARENT_OBSERVATION_NAMES_COLLECTED_IN_PREDICATE.clear(); - }; - } - -} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/observability/TestConfig.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observability/TestConfig.java deleted file mode 100644 index af6c481cad..0000000000 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/observability/TestConfig.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022-2024 the original author 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.redis.connection.lettuce.observability; - -import io.lettuce.core.resource.ClientResources; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.ObservationRegistry; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.SettingsUtils; -import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.test.extension.ShutdownQueue; - -/** - * @author Mark Paluch - * @author Yanming Zhou - */ -@Configuration -class TestConfig { - - static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry(); - static final ObservationRegistry OBSERVATION_REGISTRY = ObservationRegistry.create(); - static final List PARENT_OBSERVATION_NAMES_COLLECTED_IN_PREDICATE = new ArrayList<>(); - - static { - OBSERVATION_REGISTRY.observationConfig().observationHandler(new DefaultMeterObservationHandler(METER_REGISTRY)); - OBSERVATION_REGISTRY.observationConfig().observationPredicate((name, context) -> { - if (context.getParentObservation() != null) { - PARENT_OBSERVATION_NAMES_COLLECTED_IN_PREDICATE.add(context.getParentObservation().getContextView().getName()); - } - return true; - }); - } - - @Bean(destroyMethod = "timer") - ClientResources clientResources(ObservationRegistry observationRegistry) { - - ClientResources resources = ClientResources.builder() - .tracing(new MicrometerTracingAdapter(observationRegistry, "Redis", true)).build(); - - ShutdownQueue.register(() -> resources.shutdown(0, 0, TimeUnit.MILLISECONDS)); - return resources; - } - - @Bean - LettuceConnectionFactory connectionFactory(ClientResources clientResources) { - - LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() - .shutdownTimeout(Duration.ZERO).shutdownQuietPeriod(Duration.ZERO) - .clientResources(clientResources).build(); - - return new LettuceConnectionFactory(SettingsUtils.standaloneConfiguration(), clientConfiguration); - } - - @Bean - ObservationRegistry registry() { - return OBSERVATION_REGISTRY; - } -} diff --git a/src/test/java/org/springframework/data/redis/connection/stream/StreamReadOptionsUnitTests.java b/src/test/java/org/springframework/data/redis/connection/stream/StreamReadOptionsUnitTests.java index 4ee7bc9dfb..78fa69b066 100644 --- a/src/test/java/org/springframework/data/redis/connection/stream/StreamReadOptionsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/stream/StreamReadOptionsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/core/AbstractOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java index 4fdc618f8a..485b6b1414 100644 --- a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/BoundOperationsProxyFactoryRuntimeHintTests.java b/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryRuntimeHintTests.java index c7d763a449..ffd9e4f772 100644 --- a/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryRuntimeHintTests.java +++ b/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryRuntimeHintTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/core/BoundOperationsProxyFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryUnitTests.java index 5b98034fbd..86e53f174a 100644 --- a/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/BoundOperationsProxyFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/redis/core/ConnectionMockingRedisTemplate.java b/src/test/java/org/springframework/data/redis/core/ConnectionMockingRedisTemplate.java index af5f87b504..c156421f6a 100644 --- a/src/test/java/org/springframework/data/redis/core/ConnectionMockingRedisTemplate.java +++ b/src/test/java/org/springframework/data/redis/core/ConnectionMockingRedisTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/test/java/org/springframework/data/redis/core/ConnectionSplittingInterceptorUnitTests.java b/src/test/java/org/springframework/data/redis/core/ConnectionSplittingInterceptorUnitTests.java index c8b83d3a07..c0796cd83b 100644 --- a/src/test/java/org/springframework/data/redis/core/ConnectionSplittingInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/ConnectionSplittingInterceptorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/ConvertingCursorUnitTests.java b/src/test/java/org/springframework/data/redis/core/ConvertingCursorUnitTests.java index a228f7b589..fb0e9a5515 100644 --- a/src/test/java/org/springframework/data/redis/core/ConvertingCursorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/ConvertingCursorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultClusterOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java index e2deedc846..643b01ffe6 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/DefaultGeoOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java index d299c8378f..0000db4885 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/DefaultHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java index fe79d8d014..4982e72973 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -19,17 +19,26 @@ import static org.assertj.core.api.Assumptions.*; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension; +import org.springframework.data.redis.core.ExpireChanges.ExpiryChangeState; +import org.springframework.data.redis.core.types.Expirations.TimeToLive; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.RedisStanalone; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -39,6 +48,7 @@ * * @author Jennifer Hickey * @author Christoph Strobl + * @author Tihomir Mateev * @param Key type * @param Hash key type * @param Hash value type @@ -131,7 +141,6 @@ void testHScanReadsValuesFully() throws IOException { hashOps.put(key, key1, val1); hashOps.put(key, key2, val2); - long count = 0; try (Cursor> it = hashOps.scan(key, ScanOptions.scanOptions().count(1).build())) { @@ -202,4 +211,196 @@ void randomValue() { Map values = hashOps.randomEntries(key, 10); assertThat(values).hasSize(2).containsEntry(key1, val1).containsEntry(key2, val2); } + + @EnabledOnCommand("HEXPIRE") // GH-3054 + @ParameterizedRedisTest + void testExpireAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + assertThat(redisTemplate.opsForHash().expire(key, Duration.ofMillis(500), List.of(key1))) + .satisfies(ExpireChanges::allOk); + + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1))).satisfies(expirations -> { + + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 1L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofSeconds(1)); + }); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + assertThat(redisTemplate.opsForHash().expire(key, Duration.ofSeconds(5), List.of(key1, key2))) + .satisfies(changes -> { + assertThat(changes.allOk()).isTrue(); + assertThat(changes.stateOf(key1)).isEqualTo(ExpiryChangeState.OK); + assertThat(changes.ok()).containsExactlyInAnyOrder(key1, key2); + assertThat(changes.missed()).isEmpty(); + assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); + }); + + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2))) + .satisfies(expirations -> { + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 5L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); + }); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testBoundExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + BoundHashOperations hashOps = redisTemplate.boundHashOps(key); + BoundHashFieldExpirationOperations exp = hashOps.hashExpiration(key1, key2); + + assertThat(exp.expire(Duration.ofSeconds(5))).satisfies(changes -> { + assertThat(changes.allOk()).isTrue(); + assertThat(changes.stateOf(key1)).isEqualTo(ExpiryChangeState.OK); + assertThat(changes.ok()).containsExactlyInAnyOrder(key1, key2); + assertThat(changes.missed()).isEmpty(); + assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); + }); + + assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(expirations -> { + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 5L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); + }); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireAtAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(500), List.of(key1, key2))) + .satisfies(ExpireChanges::allOk); + + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.MILLISECONDS, List.of(key1, key2))) + .satisfies(expirations -> { + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.MILLISECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 500L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofMillis(500)); + }); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void expireThrowsErrorOfNanoPrecision() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.NANOSECONDS, List.of(key1))); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireWithOptionsNone() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + ExpireChanges expire = redisTemplate.opsForHash().expire(key, + org.springframework.data.redis.core.types.Expiration.seconds(20), ExpirationOptions.none(), List.of(key1)); + + assertThat(expire.allOk()).isTrue(); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireWithOptions() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(20), + ExpirationOptions.none(), List.of(key1)); + redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(60), + ExpirationOptions.none(), List.of(key2)); + + ExpireChanges changes = redisTemplate.opsForHash().expire(key, + org.springframework.data.redis.core.types.Expiration.seconds(30), ExpirationOptions.builder().gt().build(), + List.of(key1, key2)); + + assertThat(changes.ok()).containsExactly(key1); + assertThat(changes.skipped()).containsExactly(key2); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testPersistAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(800), List.of(key1, key2))) + .satisfies(ExpireChanges::allOk); + + assertThat(redisTemplate.opsForHash().persist(key, List.of(key2))).satisfies(ExpireChanges::allOk); + + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2))).satisfies(expirations -> { + assertThat(expirations.expirationOf(key1).isPersistent()).isFalse(); + assertThat(expirations.expirationOf(key2).isPersistent()).isTrue(); + }); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultHyperLogLogOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHyperLogLogOperationsIntegrationTests.java index 3ac0a643d4..be31c3932e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHyperLogLogOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHyperLogLogOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java index 559551a4b0..4965b45032 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -40,6 +40,7 @@ * @author Jennifer Hickey * @author Thomas Darimont * @author Christoph Strobl + * @author Lee Jaeheon * @param Key test * @param Value test */ @@ -55,8 +56,7 @@ public class DefaultListOperationsIntegrationIntegrationTests { private final ListOperations listOps; public DefaultListOperationsIntegrationIntegrationTests(RedisTemplate redisTemplate, - ObjectFactory keyFactory, - ObjectFactory valueFactory) { + ObjectFactory keyFactory, ObjectFactory valueFactory) { this.redisTemplate = redisTemplate; this.keyFactory = keyFactory; @@ -346,6 +346,34 @@ void moveWithTimeout() { assertThat(listOps.range(target, 0, -1)).containsExactly(v4, v1); } + @ParameterizedRedisTest // GH-2937 + void getFirst() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOps.rightPush(key, v1); + listOps.rightPush(key, v2); + listOps.rightPush(key, v3); + assertThat(listOps.getFirst(key)).isEqualTo(v1); + } + + @ParameterizedRedisTest // GH-2937 + void getLast() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOps.rightPush(key, v1); + listOps.rightPush(key, v2); + listOps.rightPush(key, v3); + assertThat(listOps.getLast(key)).isEqualTo(v3); + } + @ParameterizedRedisTest // DATAREDIS-1196 @EnabledOnCommand("LPOS") void indexOf() { @@ -376,4 +404,5 @@ void lastIndexOf() { assertThat(listOps.rightPush(key, v3)).isEqualTo(Long.valueOf(4)); assertThat(listOps.lastIndexOf(key, v1)).isEqualTo(2); } + } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java index cd9411b4ef..795e1a16af 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java index d844370bfb..a387f2ae3c 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,24 +19,29 @@ import static org.assertj.core.api.Assumptions.*; import static org.junit.jupiter.api.condition.OS.*; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.springframework.data.redis.connection.convert.Converters; import reactor.test.StepVerifier; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.condition.DisabledOnOs; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -501,6 +506,148 @@ void scan() { .verifyComplete(); } + @EnabledOnCommand("HEXPIRE") // GH-3054 + @ParameterizedRedisTest + void testExpireAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + putAll(key, key1, val1, key2, val2); + + hashOperations.expire(key, Duration.ofMillis(1500), List.of(key1)) // + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + hashOperations.getTimeToLive(key, List.of(key1)) // + .as(StepVerifier::create) // + .assertNext(it -> { + assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireWithOptions() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + putAll(key, key1, val1, key2, val2); + + hashOperations + .expire(key, org.springframework.data.redis.core.types.Expiration.seconds(20), ExpirationOptions.none(), + List.of(key1)) + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + hashOperations + .expire(key, org.springframework.data.redis.core.types.Expiration.seconds(60), ExpirationOptions.none(), + List.of(key2)) + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + hashOperations + .expire(key, org.springframework.data.redis.core.types.Expiration.seconds(30), + ExpirationOptions.builder().gt().build(), List.of(key1, key2)) + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.ok()).containsExactly(key1); + assertThat(changes.skipped()).containsExactly(key2); + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + putAll(key, key1, val1, key2, val2); + + hashOperations.expire(key, Duration.ofSeconds(5), List.of(key1, key2)) // + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + hashOperations.getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2)) // + .as(StepVerifier::create) // + .assertNext(it -> { + assertThat(it.expirationOf(key1).raw()).isBetween(0L, 5L); + assertThat(it.expirationOf(key2).raw()).isBetween(0L, 5L); + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireAtAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + putAll(key, key1, val1, key2, val2); + + redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(1500), List.of(key1, key2)) + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// + .assertNext(it -> { + assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); + assertThat(it.expirationOf(key2).raw()).isBetween(0L, 2L); + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testPersistAndGetExpireMillis() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + + putAll(key, key1, val1, key2, val2); + + redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(1500), List.of(key1, key2)) + .as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + redisTemplate.opsForHash().persist(key, List.of(key1, key2)).as(StepVerifier::create)// + .assertNext(changes -> { + assertThat(changes.allOk()).isTrue(); + }).verifyComplete(); + + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// + .assertNext(expirations -> { + assertThat(expirations.persistent()).contains(key1, key2); + }).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void delete() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java index a52d2e20d3..183704a76e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveListOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java index 92fcd6d783..f42aaa9af4 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,8 +15,10 @@ */ package org.springframework.data.redis.core; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import reactor.test.StepVerifier; import java.time.Duration; import java.util.Collection; @@ -33,8 +35,6 @@ import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; -import reactor.test.StepVerifier; - /** * Integration tests for {@link DefaultReactiveListOperations}. * @@ -417,6 +417,32 @@ void index() { listOperations.index(key, 1).as(StepVerifier::create).expectNext(value2).verifyComplete(); } + @ParameterizedRedisTest // GH-2937 + void getFirst() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOperations.rightPushAll(key, v1, v2, v3).as(StepVerifier::create).expectNext(3L).verifyComplete(); + + listOperations.getFirst(key).as(StepVerifier::create).expectNext(v1).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2937 + void getLast() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOperations.rightPushAll(key, v1, v2, v3).as(StepVerifier::create).expectNext(3L).verifyComplete(); + + listOperations.getLast(key).as(StepVerifier::create).expectNext(v3).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-1196 @EnabledOnCommand("LPOS") void indexOf() { @@ -469,16 +495,9 @@ void leftPopWithCount() { V value2 = valueFactory.instance(); V value3 = valueFactory.instance(); - listOperations.leftPushAll(key, value1, value2, value3) - .as(StepVerifier::create) - .expectNext(3L) - .verifyComplete(); + listOperations.leftPushAll(key, value1, value2, value3).as(StepVerifier::create).expectNext(3L).verifyComplete(); - listOperations.leftPop(key, 2) - .as(StepVerifier::create) - .expectNext(value3) - .expectNext(value2) - .verifyComplete(); + listOperations.leftPop(key, 2).as(StepVerifier::create).expectNext(value3).expectNext(value2).verifyComplete(); } @ParameterizedRedisTest // DATAREDIS-602 @@ -505,16 +524,9 @@ void rightPopWithCount() { V value2 = valueFactory.instance(); V value3 = valueFactory.instance(); - listOperations.rightPushAll(key, value3, value2, value1) - .as(StepVerifier::create) - .expectNext(3L) - .verifyComplete(); + listOperations.rightPushAll(key, value3, value2, value1).as(StepVerifier::create).expectNext(3L).verifyComplete(); - listOperations.rightPop(key, 2) - .as(StepVerifier::create) - .expectNext(value1) - .expectNext(value2) - .verifyComplete(); + listOperations.rightPop(key, 2).as(StepVerifier::create).expectNext(value1).expectNext(value2).verifyComplete(); } @ParameterizedRedisTest // DATAREDIS-602 diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java index 6ffeb10725..37b497f893 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveStreamOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveStreamOperationsIntegrationTests.java index c6040d795c..f8e5360e49 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveStreamOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveStreamOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.stream.Consumer; import org.springframework.data.redis.connection.stream.MapRecord; import org.springframework.data.redis.connection.stream.ReadOffset; @@ -61,6 +62,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Marcin Zielinski + * @author jinkshower */ @MethodSource("testParams") @SuppressWarnings("unchecked") @@ -80,12 +82,6 @@ public class DefaultReactiveStreamOperationsIntegrationTests { return ReactiveOperationsTestParams.testParams(); } - /** - * @param redisTemplate - * @param keyFactory - * @param valueFactory - * @param label parameterized test label, no further use besides that. - */ public DefaultReactiveStreamOperationsIntegrationTests(Fixture fixture) { this.serializer = fixture.getSerializer(); @@ -192,6 +188,153 @@ void addShouldAddReadSimpleMessageWithRawSerializer() { .verifyComplete(); } + @ParameterizedRedisTest // GH-2915 + void addMaxLenShouldLimitMessagesSize() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = valueFactory.instance(); + + streamOperations.add(key, Collections.singletonMap(hashKey, value)).block(); + + HV newValue = valueFactory.instance(); + XAddOptions options = XAddOptions.maxlen(1).approximateTrimming(false); + + RecordId messageId = streamOperations.add(key, Collections.singletonMap(hashKey, newValue), options).block(); + + streamOperations.range(key, Range.unbounded()).as(StepVerifier::create).consumeNextWith(actual -> { + + assertThat(actual.getId()).isEqualTo(messageId); + assertThat(actual.getStream()).isEqualTo(key); + assertThat(actual).hasSize(1); + + if (!(key instanceof byte[] || value instanceof byte[])) { + assertThat(actual.getValue()).containsEntry(hashKey, newValue); + } + + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2915 + void addMaxLenShouldLimitSimpleMessagesSize() { + + assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer) + && !(serializer instanceof GenericJackson2JsonRedisSerializer) + && !(serializer instanceof JdkSerializationRedisSerializer) && !(serializer instanceof OxmSerializer)); + + K key = keyFactory.instance(); + HV value = valueFactory.instance(); + + streamOperations.add(StreamRecords.objectBacked(value).withStreamKey(key)).block(); + + HV newValue = valueFactory.instance(); + XAddOptions options = XAddOptions.maxlen(1).approximateTrimming(false); + + RecordId messageId = streamOperations.add(StreamRecords.objectBacked(newValue).withStreamKey(key), options).block(); + + streamOperations.range((Class) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) + .consumeNextWith(actual -> { + + assertThat(actual.getId()).isEqualTo(messageId); + assertThat(actual.getStream()).isEqualTo(key); + assertThat(actual.getValue()).isEqualTo(newValue); + + }).expectNextCount(0).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2915 + void addMaxLenShouldLimitSimpleMessageWithRawSerializerSize() { + + assumeTrue(!(serializer instanceof Jackson2JsonRedisSerializer) + && !(serializer instanceof GenericJackson2JsonRedisSerializer)); + + SerializationPair keySerializer = redisTemplate.getSerializationContext().getKeySerializationPair(); + + RedisSerializationContext serializationContext = RedisSerializationContext + . newSerializationContext(StringRedisSerializer.UTF_8).key(keySerializer) + .hashValue(SerializationPair.raw()).hashKey(SerializationPair.raw()).build(); + + ReactiveRedisTemplate raw = new ReactiveRedisTemplate<>(redisTemplate.getConnectionFactory(), + serializationContext); + + K key = keyFactory.instance(); + Person value = new PersonObjectFactory().instance(); + + raw.opsForStream().add(StreamRecords.objectBacked(value).withStreamKey(key)).block(); + + Person newValue = new PersonObjectFactory().instance(); + XAddOptions options = XAddOptions.maxlen(1).approximateTrimming(false); + + RecordId messageId = raw.opsForStream().add(StreamRecords.objectBacked(newValue).withStreamKey(key), options) + .block(); + + raw.opsForStream().range((Class) value.getClass(), key, Range.unbounded()).as(StepVerifier::create) + .consumeNextWith(it -> { + + assertThat(it.getId()).isEqualTo(messageId); + assertThat(it.getStream()).isEqualTo(key); + assertThat(it.getValue()).isEqualTo(newValue); + + }).expectNextCount(0).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2915 + void addMinIdShouldEvictLowerIdMessages() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = valueFactory.instance(); + + streamOperations.add(key, Collections.singletonMap(hashKey, value)).block(); + RecordId messageId1 = streamOperations.add(key, Collections.singletonMap(hashKey, value)).block(); + + XAddOptions options = XAddOptions.none().minId(messageId1); + + RecordId messageId2 = streamOperations.add(key, Collections.singletonMap(hashKey, value), options).block(); + + streamOperations.range(key, Range.unbounded()).as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual.getId()).isEqualTo(messageId1); + assertThat(actual.getStream()).isEqualTo(key); + }).consumeNextWith(actual -> { + assertThat(actual.getId()).isEqualTo(messageId2); + assertThat(actual.getStream()).isEqualTo(key); + }).expectNextCount(0).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2915 + void addMakeNoStreamShouldNotCreateStreamWhenNoStreamExists() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = valueFactory.instance(); + + XAddOptions options = XAddOptions.makeNoStream(); + + streamOperations.add(key, Collections.singletonMap(hashKey, value), options).block(); + + streamOperations.size(key).as(StepVerifier::create).expectNext(0L).verifyComplete(); + + streamOperations.range(key, Range.unbounded()).as(StepVerifier::create).expectNextCount(0L).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2915 + void addMakeNoStreamShouldCreateStreamWhenStreamExists() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = valueFactory.instance(); + + streamOperations.add(key, Collections.singletonMap(hashKey, value)).block(); + + XAddOptions options = XAddOptions.makeNoStream(); + + streamOperations.add(key, Collections.singletonMap(hashKey, value), options).block(); + + streamOperations.size(key).as(StepVerifier::create).expectNext(2L).verifyComplete(); + + streamOperations.range(key, Range.unbounded()).as(StepVerifier::create).expectNextCount(2L).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-864 void rangeShouldReportMessages() { @@ -359,7 +502,6 @@ void pendingShouldReadMessageDetails() { assertThat(pending.get(0).getConsumerName()).isEqualTo("my-consumer"); assertThat(pending.get(0).getTotalDeliveryCount()).isOne(); }).verifyComplete(); - } @ParameterizedRedisTest // GH-2465 @@ -384,6 +526,5 @@ void claimShouldReadMessageDetails() { assertThat(claimed.getValue()).isEqualTo(content); assertThat(claimed.getId()).isEqualTo(messageId); }).verifyComplete(); - } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java index b1f44caf21..7dfeb0266d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java index 6512faaae8..d0e1afa11a 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/DefaultSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultSetOperationsIntegrationTests.java index d4bf27f48b..67e31a2163 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultSetOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/DefaultStreamOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultStreamOperationsIntegrationTests.java index eb2cb33079..6dc4f8dc3d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultStreamOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultStreamOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -32,10 +32,20 @@ import org.springframework.data.redis.Person; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStreamCommands.XAddOptions; import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.extension.LettuceConnectionFactoryExtension; -import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.PendingMessagesSummary; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamReadOptions; +import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.condition.EnabledOnRedisDriver; import org.springframework.data.redis.test.condition.EnabledOnRedisVersion; @@ -51,6 +61,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Marcin Zielinski + * @author jinkshower */ @MethodSource("testParams") @EnabledOnCommand("XADD") @@ -65,7 +76,7 @@ public class DefaultStreamOperationsIntegrationTests { private final StreamOperations streamOps; public DefaultStreamOperationsIntegrationTests(RedisTemplate redisTemplate, ObjectFactory keyFactory, - ObjectFactory objectFactory) { + ObjectFactory objectFactory) { this.redisTemplate = redisTemplate; this.connectionFactory = redisTemplate.getRequiredConnectionFactory(); @@ -81,7 +92,7 @@ public static Collection testParams() { params.addAll(AbstractOperationsTestParams .testParams(JedisConnectionFactoryExtension.getConnectionFactory(RedisStanalone.class))); - if(RedisDetector.isClusterAvailable()) { + if (RedisDetector.isClusterAvailable()) { params.addAll(AbstractOperationsTestParams .testParams(JedisConnectionFactoryExtension.getConnectionFactory(RedisCluster.class))); } @@ -89,7 +100,7 @@ public static Collection testParams() { params.addAll(AbstractOperationsTestParams .testParams(LettuceConnectionFactoryExtension.getConnectionFactory(RedisStanalone.class))); - if(RedisDetector.isClusterAvailable()) { + if (RedisDetector.isClusterAvailable()) { params.addAll(AbstractOperationsTestParams .testParams(LettuceConnectionFactoryExtension.getConnectionFactory(RedisCluster.class))); } @@ -149,6 +160,155 @@ void addShouldAddReadSimpleMessage() { assertThat(message.getValue()).isEqualTo(value); } + @ParameterizedRedisTest // GH-2915 + void addMaxLenShouldLimitMessagesSize() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = hashValueFactory.instance(); + + streamOps.add(key, Collections.singletonMap(hashKey, value)); + + HV newValue = hashValueFactory.instance(); + + XAddOptions options = XAddOptions.maxlen(1).approximateTrimming(false); + + RecordId messageId = streamOps.add(key, Collections.singletonMap(hashKey, newValue), options); + + List> messages = streamOps.range(key, Range.unbounded()); + + assertThat(messages).hasSize(1); + + MapRecord message = messages.get(0); + + assertThat(message.getId()).isEqualTo(messageId); + assertThat(message.getStream()).isEqualTo(key); + + if (!(key instanceof byte[] || value instanceof byte[])) { + assertThat(message.getValue()).containsEntry(hashKey, newValue); + } + } + + @ParameterizedRedisTest // GH-2915 + void addMaxLenShouldLimitSimpleMessagesSize() { + + K key = keyFactory.instance(); + HV value = hashValueFactory.instance(); + + streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); + + HV newValue = hashValueFactory.instance(); + + XAddOptions options = XAddOptions.maxlen(1).approximateTrimming(false); + + RecordId messageId = streamOps.add(StreamRecords.objectBacked(newValue).withStreamKey(key), options); + + List> messages = streamOps.range((Class) value.getClass(), key, Range.unbounded()); + + assertThat(messages).hasSize(1); + + ObjectRecord message = messages.get(0); + + assertThat(message.getId()).isEqualTo(messageId); + assertThat(message.getStream()).isEqualTo(key); + + assertThat(message.getValue()).isEqualTo(newValue); + } + + @ParameterizedRedisTest // GH-2915 + void addMinIdShouldEvictLowerIdMessages() { + + K key = keyFactory.instance(); + HK hashKey = hashKeyFactory.instance(); + HV value = hashValueFactory.instance(); + + streamOps.add(key, Collections.singletonMap(hashKey, value)); + RecordId messageId1 = streamOps.add(key, Collections.singletonMap(hashKey, value)); + + XAddOptions options = XAddOptions.none().minId(messageId1); + + RecordId messageId2 = streamOps.add(key, Collections.singletonMap(hashKey, value), options); + + List> messages = streamOps.range(key, Range.unbounded()); + + assertThat(messages).hasSize(2); + + MapRecord message1 = messages.get(0); + + assertThat(message1.getId()).isEqualTo(messageId1); + assertThat(message1.getStream()).isEqualTo(key); + + MapRecord message2 = messages.get(1); + + assertThat(message2.getId()).isEqualTo(messageId2); + assertThat(message2.getStream()).isEqualTo(key); + + if (!(key instanceof byte[] || value instanceof byte[])) { + assertThat(message1.getValue()).containsEntry(hashKey, value); + assertThat(message2.getValue()).containsEntry(hashKey, value); + } + } + + @ParameterizedRedisTest // GH-2915 + void addMinIdShouldEvictLowerIdSimpleMessages() { + + K key = keyFactory.instance(); + HV value = hashValueFactory.instance(); + + streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); + RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); + + XAddOptions options = XAddOptions.none().minId(messageId1); + + RecordId messageId2 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key), options); + + List> messages = streamOps.range((Class) value.getClass(), key, Range.unbounded()); + + assertThat(messages).hasSize(2); + + ObjectRecord message1 = messages.get(0); + + assertThat(message1.getId()).isEqualTo(messageId1); + assertThat(message1.getStream()).isEqualTo(key); + assertThat(message1.getValue()).isEqualTo(value); + + ObjectRecord message2 = messages.get(1); + + assertThat(message2.getId()).isEqualTo(messageId2); + assertThat(message2.getStream()).isEqualTo(key); + assertThat(message2.getValue()).isEqualTo(value); + } + + @ParameterizedRedisTest // GH-2915 + void addMakeNoStreamShouldNotCreateStreamWhenNoStreamExists() { + + K key = keyFactory.instance(); + HV value = hashValueFactory.instance(); + + XAddOptions options = XAddOptions.makeNoStream(); + + streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key), options); + + assertThat(streamOps.size(key)).isZero(); + assertThat(streamOps.range(key, Range.unbounded())).isEmpty(); + } + + @ParameterizedRedisTest // GH-2915 + void addMakeNoStreamShouldCreateStreamWhenStreamExists() { + + K key = keyFactory.instance(); + HV value = hashValueFactory.instance(); + + streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); + + XAddOptions options = XAddOptions.makeNoStream(); + + streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key), options); + + assertThat(streamOps.size(key)).isEqualTo(2); + assertThat(streamOps.range(key, Range.unbounded())).hasSize(2); + } + @ParameterizedRedisTest // DATAREDIS-864 void simpleMessageReadWriteSymmetry() { @@ -305,7 +465,8 @@ void readShouldReadSimpleMessage() { RecordId messageId1 = streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); streamOps.add(StreamRecords.objectBacked(value).withStreamKey(key)); - List> messages = streamOps.read((Class) value.getClass(), StreamOffset.create(key, ReadOffset.from("0-0"))); + List> messages = streamOps.read((Class) value.getClass(), + StreamOffset.create(key, ReadOffset.from("0-0"))); assertThat(messages).hasSize(2); @@ -384,8 +545,7 @@ void pendingShouldReadMessageSummary() { RecordId messageId = streamOps.add(key, Collections.singletonMap(hashKey, value)); streamOps.createGroup(key, ReadOffset.from("0-0"), "my-group"); - streamOps.read(Consumer.from("my-group", "my-consumer"), - StreamOffset.create(key, ReadOffset.lastConsumed())); + streamOps.read(Consumer.from("my-group", "my-consumer"), StreamOffset.create(key, ReadOffset.lastConsumed())); PendingMessagesSummary pending = streamOps.pending(key, "my-group"); @@ -403,8 +563,7 @@ void pendingShouldReadMessageDetails() { RecordId messageId = streamOps.add(key, Collections.singletonMap(hashKey, value)); streamOps.createGroup(key, ReadOffset.from("0-0"), "my-group"); - streamOps.read(Consumer.from("my-group", "my-consumer"), - StreamOffset.create(key, ReadOffset.lastConsumed())); + streamOps.read(Consumer.from("my-group", "my-consumer"), StreamOffset.create(key, ReadOffset.lastConsumed())); PendingMessages pending = streamOps.pending(key, "my-group", Range.unbounded(), 10L); diff --git a/src/test/java/org/springframework/data/redis/core/DefaultTypedTupleUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultTypedTupleUnitTests.java index fa8beb2b52..f0ae042f65 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultTypedTupleUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultTypedTupleUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java index e539412d5a..abc601bb8d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -316,6 +316,32 @@ void testSetWithExpirationWithTimeUnitMilliseconds() { await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key)); } + @ParameterizedRedisTest + void testSetGetWithExpiration() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1); + assertThat(valueOps.get(key)).isEqualTo(value2); + } + + @ParameterizedRedisTest + void testSetGetWithExpirationDuration() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1); + assertThat(valueOps.get(key)).isEqualTo(value2); + } + @ParameterizedRedisTest void testAppend() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java index 1a84eb956d..373542bd50 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/DefaultZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java index 853b0d698f..dcfd41feb4 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/DefaultZSetOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java index 02462003c2..fdbb55b91e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java index b1d41586a1..bf7a398c1e 100644 --- a/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/IndexWriterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/MappingExpirationListenerTest.java b/src/test/java/org/springframework/data/redis/core/MappingExpirationListenerTest.java new file mode 100644 index 0000000000..c3df6384aa --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/MappingExpirationListenerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2024-2025 the original author 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.redis.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.core.convert.RedisConverter; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * @author Lucian Torje + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MappingExpirationListenerTest { + + @Mock private RedisOperations redisOperations; + @Mock private RedisConverter redisConverter; + @Mock private RedisMessageListenerContainer listenerContainer; + @Mock private Message message; + + private RedisKeyValueAdapter.MappingExpirationListener listener; + + @Test // GH-2954 + void testOnNonKeyExpiration() { + + byte[] key = "testKey".getBytes(); + when(message.getBody()).thenReturn(key); + listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter, + RedisKeyValueAdapter.ShadowCopy.ON); + + listener.onMessage(message, null); + + verify(redisOperations, times(0)).execute(any(RedisCallback.class)); + } + + @Test // GH-2954 + void testOnValidKeyExpirationWithShadowCopiesDisabled() { + + List eventList = new ArrayList<>(); + + byte[] key = "abc:testKey".getBytes(); + when(message.getBody()).thenReturn(key); + + listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter, + RedisKeyValueAdapter.ShadowCopy.OFF); + listener.setApplicationEventPublisher(eventList::add); + listener.onMessage(message, null); + + verify(redisOperations, times(1)).execute(any(RedisCallback.class)); + assertThat(eventList).hasSize(1); + assertThat(eventList.get(0)).isInstanceOf(RedisKeyExpiredEvent.class); + assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getKeyspace()).isEqualTo("abc"); + assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getId()).isEqualTo("testKey".getBytes()); + } + + @Test // GH-2954 + void testOnValidKeyExpirationWithShadowCopiesEnabled() { + + ConversionService conversionService = Mockito.mock(ConversionService.class); + List eventList = new ArrayList<>(); + + byte[] key = "abc:testKey".getBytes(); + when(message.getBody()).thenReturn(key); + when(redisConverter.getConversionService()).thenReturn(conversionService); + when(conversionService.convert(any(), eq(byte[].class))).thenReturn("foo".getBytes()); + + listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter, + RedisKeyValueAdapter.ShadowCopy.ON); + listener.setApplicationEventPublisher(eventList::add); + listener.onMessage(message, null); + + verify(redisOperations, times(2)).execute(any(RedisCallback.class)); // delete entry in index, delete phantom key + assertThat(eventList).hasSize(1); + assertThat(eventList.get(0)).isInstanceOf(RedisKeyExpiredEvent.class); + assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getKeyspace()).isEqualTo("abc"); + assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getId()).isEqualTo("testKey".getBytes()); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateIntegrationTests.java index 7981efec59..c0ad3a2893 100644 --- a/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java index 8364be9311..77895e3bb2 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/ReactiveRedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java index 37fabc7b30..f887c1ba22 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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,19 +24,23 @@ import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.Person; import org.springframework.data.redis.PersonObjectFactory; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; import org.springframework.data.redis.connection.ReactiveSubscription.ChannelMessage; import org.springframework.data.redis.connection.ReactiveSubscription.Message; @@ -45,6 +49,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.ReactiveOperationsTestParams.Fixture; import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisElementReader; @@ -61,6 +66,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Dahye Anne Lee */ @MethodSource("testParams") public class ReactiveRedisTemplateIntegrationTests { @@ -128,6 +134,30 @@ void exists() { redisTemplate.hasKey(key).as(StepVerifier::create).expectNext(true).verifyComplete(); } + @ParameterizedRedisTest // GH-2883 + void countExistingKeysIfValidKeyExists() { + + K key = keyFactory.instance(); + K key2 = keyFactory.instance(); + K key3 = keyFactory.instance(); + + ReactiveValueOperations ops = redisTemplate.opsForValue(); + + ops.set(key, valueFactory.instance()).as(StepVerifier::create).expectNext(true).verifyComplete(); + ops.set(key2, valueFactory.instance()).as(StepVerifier::create).expectNext(true).verifyComplete(); + ops.set(key3, valueFactory.instance()).as(StepVerifier::create).expectNext(true).verifyComplete(); + + redisTemplate.countExistingKeys(Arrays.asList(key, key2, key3)).as(StepVerifier::create).expectNext(3L) + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2883 + void countExistingKeysIfNotValidKeyExists() { + + K key = keyFactory.instance(); + redisTemplate.countExistingKeys(List.of(key)).as(StepVerifier::create).expectNext(0L).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-743 void scan() { @@ -323,6 +353,24 @@ void expire() { .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))).verifyComplete(); } + @ParameterizedRedisTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void expireWithCondition() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + redisTemplate.opsForValue().set(key, value).as(StepVerifier::create).expectNext(true).verifyComplete(); + + redisTemplate.expire(key, Expiration.seconds(10), ExpirationOptions.none()).as(StepVerifier::create) + .expectNext(ExpireChanges.ExpiryChangeState.OK).verifyComplete(); + redisTemplate.expire(key, Expiration.seconds(20), ExpirationOptions.builder().lt().build()).as(StepVerifier::create) + .expectNext(ExpireChanges.ExpiryChangeState.CONDITION_NOT_MET).verifyComplete(); + + redisTemplate.getExpire(key).as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(5))).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void preciseExpire() { @@ -337,6 +385,24 @@ void preciseExpire() { .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))).verifyComplete(); } + @ParameterizedRedisTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void preciseExpireWithCondition() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + redisTemplate.opsForValue().set(key, value).as(StepVerifier::create).expectNext(true).verifyComplete(); + + redisTemplate.expire(key, Expiration.milliseconds(10000), ExpirationOptions.none()).as(StepVerifier::create) + .expectNext(ExpireChanges.ExpiryChangeState.OK).verifyComplete(); + redisTemplate.expire(key, Expiration.milliseconds(20000), ExpirationOptions.builder().lt().build()) + .as(StepVerifier::create).expectNext(ExpireChanges.ExpiryChangeState.CONDITION_NOT_MET).verifyComplete(); + + redisTemplate.getExpire(key).as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(5))).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void expireAt() { @@ -544,4 +610,5 @@ void listenToPatternLaterShouldReceiveChannelMessagesCorrectly() { .thenCancel() // .verify(Duration.ofSeconds(3)); } + } diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateUnitTests.java b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateUnitTests.java index f27299b272..de6deaa4a0 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/java/org/springframework/data/redis/core/ReactiveStringRedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/ReactiveStringRedisTemplateIntegrationTests.java index b41ee3dc4e..ea3ec41dd8 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveStringRedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveStringRedisTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/RedisAccessorUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisAccessorUnitTests.java index d1f9f4d800..63f7616908 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisAccessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/RedisClusterTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateIntegrationTests.java index 4a17444966..c2b5f55b01 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisCommandUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java index 92c4ab693e..e4d376de82 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/RedisConnectionUtilsUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisConnectionUtilsUnitTests.java index 19854032ff..2364624d06 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisConnectionUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisConnectionUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/core/RedisKeyExpiredEventUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java index 6ab8ab530b..4d28d2632b 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java index 1e23e01c6e..a445a6bb17 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,19 +82,17 @@ void setUp() { adapter = new RedisKeyValueAdapter(template, mappingContext); adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP); adapter.afterPropertiesSet(); + adapter.start(); template.execute((RedisCallback) connection -> { connection.flushDb(); return null; }); - RedisConnection connection = template.getConnectionFactory().getConnection(); - - try { + try (RedisConnection connection = template.getConnectionFactory() + .getConnection()) { connection.setConfig("notify-keyspace-events", ""); connection.setConfig("notify-keyspace-events", "KEA"); - } finally { - connection.close(); } } @@ -823,7 +821,7 @@ private static void waitUntilKeyIsGone(RedisTemplate template, String while (template.hasKey(key)) { if (waitedMs > limitMs) { - throw new TimeoutException(String.format("Key '%s' after %d %s still present", key, timeout, timeUnit)); + throw new TimeoutException("Key '%s' after %d %s still present".formatted(key, timeout, timeUnit)); } Thread.sleep(sleepMs); diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java index b992cb5fa4..4212f79642 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,6 +100,7 @@ void setUp() throws Exception { adapter = new RedisKeyValueAdapter(template, context); adapter.afterPropertiesSet(); + adapter.start(); } @AfterEach @@ -153,12 +154,32 @@ void shouldInitKeyExpirationListenerOnStartup() throws Exception { adapter = new RedisKeyValueAdapter(template, context); adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP); adapter.afterPropertiesSet(); + adapter.start(); KeyExpirationEventMessageListener listener = ((AtomicReference) getField(adapter, "expirationListener")).get(); assertThat(listener).isNotNull(); } + @Test // GH-2957 + void adapterShouldBeRestartable() throws Exception { + + adapter.destroy(); + + adapter = new RedisKeyValueAdapter(template, context); + adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP); + adapter.afterPropertiesSet(); + adapter.start(); + adapter.stop(); + + assertThat(((AtomicReference) getField(adapter, "expirationListener")).get()) + .isNull(); + + adapter.start(); + assertThat(((AtomicReference) getField(adapter, "expirationListener")).get()) + .isNotNull(); + } + @Test // DATAREDIS-491 void shouldInitKeyExpirationListenerOnFirstPutWithTtl() throws Exception { diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java index ab19b6522c..fb464d61a3 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/RedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java index bbc18c6f0e..56f99db3ba 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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. @@ -34,6 +34,7 @@ import org.springframework.data.redis.Person; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ExpirationOptions; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.StringRedisConnection; @@ -42,11 +43,13 @@ import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.data.redis.core.query.SortQueryBuilder; import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.test.condition.EnabledIfLongRunningTest; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.LettuceTestClientResources; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -503,6 +506,46 @@ void testExpireAndGetExpireMillis() { assertThat(redisTemplate.getExpire(key1, TimeUnit.MILLISECONDS)).isGreaterThan(0L); } + @ParameterizedRedisTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void testBoundExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + redisTemplate.boundValueOps(key).set(value1); + + BoundKeyExpirationOperations exp = redisTemplate.expiration(key); + + assertThat(exp.expire(Duration.ofSeconds(5))).isEqualTo(ExpireChanges.ExpiryChangeState.OK); + + assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(ttl -> { + assertThat(ttl.isPersistent()).isFalse(); + assertThat(ttl.value()).isGreaterThan(1); + }); + } + + @ParameterizedRedisTest // GH-3114 + @EnabledOnCommand("SPUBLISH") // Redis 7.0 + void testBoundExpireWithConditionsAndGetExpireSeconds() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + redisTemplate.boundValueOps(key).set(value1); + + BoundKeyExpirationOperations exp = redisTemplate.expiration(key); + + assertThat(exp.expire(Duration.ofSeconds(5))).isEqualTo(ExpireChanges.ExpiryChangeState.OK); + assertThat(exp.expire(Expiration.from(Duration.ofSeconds(1)), ExpirationOptions.builder().gt().build())) + .isEqualTo(ExpireChanges.ExpiryChangeState.CONDITION_NOT_MET); + assertThat(exp.expire(Expiration.from(Duration.ofSeconds(10)), ExpirationOptions.builder().gt().build())) + .isEqualTo(ExpireChanges.ExpiryChangeState.OK); + + assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(ttl -> { + assertThat(ttl.isPersistent()).isFalse(); + assertThat(ttl.value()).isGreaterThan(5); + }); + } + @ParameterizedRedisTest void testGetExpireNoTimeUnit() { K key1 = keyFactory.instance(); @@ -571,6 +614,23 @@ void testGetExpireMillis() { assertThat(ttl).isLessThan(25L); } + @ParameterizedRedisTest // GH-3017 + void testSetGetExpireMillis() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + redisTemplate.boundValueOps(key).set(value1); + + V oldValue = redisTemplate.boundValueOps(key).setGet(value2, 1, TimeUnit.DAYS); + redisTemplate.expire(key, 1, TimeUnit.DAYS); + Long ttl = redisTemplate.getExpire(key, TimeUnit.HOURS); + + assertThat(oldValue).isEqualTo(value1); + assertThat(ttl).isGreaterThanOrEqualTo(23L); + assertThat(ttl).isLessThan(25L); + } + @ParameterizedRedisTest // DATAREDIS-611 void testGetExpireDuration() { diff --git a/src/test/java/org/springframework/data/redis/core/RedisTemplateUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateUnitTests.java index 9b22293cf6..06bea63fdc 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisTemplateUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/ScanCursorUnitTests.java b/src/test/java/org/springframework/data/redis/core/ScanCursorUnitTests.java index bcbecdea3a..c9d8863791 100644 --- a/src/test/java/org/springframework/data/redis/core/ScanCursorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/ScanCursorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/SessionUnitTests.java b/src/test/java/org/springframework/data/redis/core/SessionUnitTests.java index 7b87af8ff0..20ebdd5890 100644 --- a/src/test/java/org/springframework/data/redis/core/SessionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/SessionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/core/TimeoutUtilsUnitTests.java b/src/test/java/org/springframework/data/redis/core/TimeoutUtilsUnitTests.java index 91ec24027f..ae35baad70 100644 --- a/src/test/java/org/springframework/data/redis/core/TimeoutUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/TimeoutUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java index 99723e9229..eddea1020d 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java index d859bee0d1..58063cb23d 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/CompositeIndexResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/convert/ConversionTestEntities.java b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java index 41ce475264..c9b5d00c66 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java +++ b/src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/DefaultRedisTypeMapperUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapperUnitTests.java index fa5943e91c..a25465545d 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapperUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/DefaultRedisTypeMapperUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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. @@ -215,7 +215,7 @@ private void writesTypeToField(Bucket bucket, Class type, @Nullable Object va assertThat(bucket.keySet()).isEmpty(); } else { - byte[] expected = value instanceof Class ? ((Class) value).getName().getBytes() : value.toString().getBytes(); + byte[] expected = value instanceof Class javaClass ? javaClass.getName().getBytes() : value.toString().getBytes(); assertThat(bucket.asMap()).containsKey(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY); assertThat(bucket.get(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY)).isEqualTo(expected); diff --git a/src/test/java/org/springframework/data/redis/core/convert/Jsr310ConvertersTest.java b/src/test/java/org/springframework/data/redis/core/convert/Jsr310ConvertersTest.java index a95ca213ee..9b06715d80 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/Jsr310ConvertersTest.java +++ b/src/test/java/org/springframework/data/redis/core/convert/Jsr310ConvertersTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/core/convert/KeyspaceIdentifierUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java index f115d1eb4e..1f965b71dd 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java index aff41be615..c70b933f07 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/PathIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java index 8fa29d33c0..8a61c7a41c 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/PathIndexResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/convert/SpelIndexResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java index 46c11631a3..153b3c9b65 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/SpelIndexResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/index/IndexConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java index 9201e0b7cb..75c5d8a85e 100644 --- a/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/index/IndexConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java index f26bbf93da..a353d50573 100644 --- a/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntityUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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 @@ * @author Christoph Strobl * @author Mark Paluch * @param - * @param */ @ExtendWith(MockitoExtension.class) class BasicRedisPersistentEntityUnitTests { @@ -66,7 +65,8 @@ void addingMultipleIdPropertiesWithoutAnExplicitOneThrowsException() { entity.addPersistentProperty(property1); assertThatExceptionOfType(MappingException.class).isThrownBy(() -> entity.addPersistentProperty(property2)) - .withMessageContaining("Attempt to add id property").withMessageContaining("but already have an property"); + .withMessageContaining("Attempt to add id property") + .withMessageContaining("but already have a property"); } @Test // DATAREDIS-425 @@ -84,7 +84,7 @@ void addingMultipleExplicitIdPropertiesThrowsException() { entity.addPersistentProperty(property1); assertThatExceptionOfType(MappingException.class).isThrownBy(() -> entity.addPersistentProperty(property2)) .withMessageContaining("Attempt to add explicit id property") - .withMessageContaining("but already have an property"); + .withMessageContaining("but already have a property"); } @Test // DATAREDIS-425 diff --git a/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java index bf3e1d2ef7..182e73214d 100644 --- a/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareKeySpaceResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java index 9d78ac4a69..29f8804012 100644 --- a/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/mapping/ConfigAwareTimeToLiveAccessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/core/script/AbstractDefaultScriptExecutorTests.java b/src/test/java/org/springframework/data/redis/core/script/AbstractDefaultScriptExecutorTests.java index e6c8b3ce36..005212248f 100644 --- a/src/test/java/org/springframework/data/redis/core/script/AbstractDefaultScriptExecutorTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/AbstractDefaultScriptExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/script/DefaultReactiveScriptExecutorTests.java b/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorTests.java index 7990d1a145..3676c27c24 100644 --- a/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/script/DefaultReactiveScriptExecutorUnitTests.java b/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorUnitTests.java index 2c68eca8d7..456eed04ce 100644 --- a/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/core/script/DefaultRedisScriptTests.java b/src/test/java/org/springframework/data/redis/core/script/DefaultRedisScriptTests.java index a8b7c14797..3cb6445d39 100644 --- a/src/test/java/org/springframework/data/redis/core/script/DefaultRedisScriptTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/DefaultRedisScriptTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/core/script/DefaultScriptExecutorUnitTests.java b/src/test/java/org/springframework/data/redis/core/script/DefaultScriptExecutorUnitTests.java index 40283a20f4..82a288fd8d 100644 --- a/src/test/java/org/springframework/data/redis/core/script/DefaultScriptExecutorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/DefaultScriptExecutorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/script/jedis/JedisDefaultScriptExecutorTests.java b/src/test/java/org/springframework/data/redis/core/script/jedis/JedisDefaultScriptExecutorTests.java index c9ba2e0eea..6488a36e02 100644 --- a/src/test/java/org/springframework/data/redis/core/script/jedis/JedisDefaultScriptExecutorTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/jedis/JedisDefaultScriptExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/script/lettuce/LettuceDefaultScriptExecutorTests.java b/src/test/java/org/springframework/data/redis/core/script/lettuce/LettuceDefaultScriptExecutorTests.java index a269a87203..8674f49458 100644 --- a/src/test/java/org/springframework/data/redis/core/script/lettuce/LettuceDefaultScriptExecutorTests.java +++ b/src/test/java/org/springframework/data/redis/core/script/lettuce/LettuceDefaultScriptExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/core/types/ExpirationUnitTests.java b/src/test/java/org/springframework/data/redis/core/types/ExpirationUnitTests.java index 681ca84b26..d9657bf247 100644 --- a/src/test/java/org/springframework/data/redis/core/types/ExpirationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/types/ExpirationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/core/types/ExpirationsUnitTest.java b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java new file mode 100644 index 0000000000..db5ba98605 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 the original author 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.redis.core.types; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import org.springframework.data.redis.core.types.Expirations.Timeouts; + +/** + * Unit test for {@link Expirations} + * + * @author Christoph Strobl + * @author Mark Paluch + */ +class ExpirationsUnitTest { + + static final String KEY_1 = "key-1"; + static final String KEY_2 = "key-2"; + static final String KEY_3 = "key-3"; + + @ParameterizedTest // GH-3054 + @EnumSource(TimeUnit.class) + void expirationMemorizesSourceUnit(TimeUnit targetUnit) { + + Expirations exp = Expirations.of(targetUnit, List.of(KEY_1), new Timeouts(TimeUnit.SECONDS, List.of(120L))); + + assertThat(exp.ttl().get(0)).satisfies(expiration -> { + assertThat(expiration.raw()).isEqualTo(120L); + assertThat(expiration.value()).isEqualTo(targetUnit.convert(120, TimeUnit.SECONDS)); + }); + } + + @Test // GH-3054 + void expirationsCategorizesElements() { + + Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); + + assertThat(exp.persistent()).containsExactly(KEY_2); + assertThat(exp.missing()).containsExactly(KEY_1); + assertThat(exp.expiring()).containsExactly(Map.entry(KEY_3, Duration.ofMinutes(2))); + } + + @Test // GH-3054 + void returnsNullForMissingElements() { + + Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); + + assertThat(exp.expirationOf("missing")).isNull(); + assertThat(exp.ttlOf("missing")).isNull(); + } + + @Test // GH-3054 + void ttlReturnsDurationForEntriesWithTimeout() { + + Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); + + assertThat(exp.ttlOf(KEY_3)).isEqualTo(Duration.ofMinutes(2)); + } + + @Test // GH-3054 + void ttlReturnsNullForPersistentAndMissingEntries() { + + Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); + + assertThat(exp.ttlOf(KEY_1)).isNull(); + assertThat(exp.ttlOf(KEY_2)).isNull(); + } + + @Test // GH-3054 + void shouldRenderToString() { + + assertThat(Expirations.TimeToLive.PERSISTENT).hasToString("PERSISTENT"); + assertThat(Expirations.TimeToLive.MISSING).hasToString("MISSING"); + assertThat(Expirations.TimeToLive.of(1, TimeUnit.SECONDS)).hasToString("1 SECONDS"); + } + + static Expirations createExpirations(Timeouts timeouts) { + + List keys = IntStream.range(1, timeouts.raw().size() + 1).mapToObj("key-%s"::formatted).toList(); + return Expirations.of(timeouts.timeUnit(), keys, timeouts); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/types/RedisClientInfoUnitTests.java b/src/test/java/org/springframework/data/redis/core/types/RedisClientInfoUnitTests.java index 893f9915ab..cdf85e7138 100644 --- a/src/test/java/org/springframework/data/redis/core/types/RedisClientInfoUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/types/RedisClientInfoUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/domain/geo/BoundingBoxUnitTests.java b/src/test/java/org/springframework/data/redis/domain/geo/BoundingBoxUnitTests.java index ab40d19bc7..5b5e642512 100644 --- a/src/test/java/org/springframework/data/redis/domain/geo/BoundingBoxUnitTests.java +++ b/src/test/java/org/springframework/data/redis/domain/geo/BoundingBoxUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/examples/ReactiveRedisApplication.java b/src/test/java/org/springframework/data/redis/examples/ReactiveRedisApplication.java index 4cac7b2eb1..1689d07a26 100644 --- a/src/test/java/org/springframework/data/redis/examples/ReactiveRedisApplication.java +++ b/src/test/java/org/springframework/data/redis/examples/ReactiveRedisApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/examples/RedisApplication.java b/src/test/java/org/springframework/data/redis/examples/RedisApplication.java index 21b01e228d..4ce9118321 100644 --- a/src/test/java/org/springframework/data/redis/examples/RedisApplication.java +++ b/src/test/java/org/springframework/data/redis/examples/RedisApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java index 1ce50709dc..ddf8fdd66b 100644 --- a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/listener/KeyExpirationEventMessageListenerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java index 6402df5c4a..bc39e548e3 100644 --- a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/listener/PubSubAwaitUtil.java b/src/test/java/org/springframework/data/redis/listener/PubSubAwaitUtil.java index 4b79d20d6a..557903967c 100644 --- a/src/test/java/org/springframework/data/redis/listener/PubSubAwaitUtil.java +++ b/src/test/java/org/springframework/data/redis/listener/PubSubAwaitUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -87,8 +87,8 @@ public static void runAndAwaitChannelSubscription(RedisConnectionFactory connect private static long numPat(RedisConnection connection) { - if (connection instanceof LettuceConnection) { - return ((Number) ((LettuceConnection) connection).execute("PUBSUB", new IntegerOutput<>(ByteArrayCodec.INSTANCE), + if (connection instanceof LettuceConnection lettuceConnection) { + return ((Number) lettuceConnection.execute("PUBSUB", new IntegerOutput<>(ByteArrayCodec.INSTANCE), "NUMPAT".getBytes())).longValue(); } @@ -98,8 +98,8 @@ private static long numPat(RedisConnection connection) { private static long numSub(RedisConnection connection, String channel) { List pubsub; - if (connection instanceof LettuceConnection) { - pubsub = (List) ((LettuceConnection) connection).execute("PUBSUB", new ArrayOutput<>(ByteArrayCodec.INSTANCE), + if (connection instanceof LettuceConnection lettuceConnection) { + pubsub = (List) lettuceConnection.execute("PUBSUB", new ArrayOutput<>(ByteArrayCodec.INSTANCE), "NUMSUB".getBytes(), channel.getBytes()); } else { pubsub = (List) connection.execute("PUBSUB", "NUMSUB".getBytes(), channel.getBytes()); diff --git a/src/test/java/org/springframework/data/redis/listener/PubSubResubscribeTests.java b/src/test/java/org/springframework/data/redis/listener/PubSubResubscribeTests.java index 378c030499..63e441d77e 100644 --- a/src/test/java/org/springframework/data/redis/listener/PubSubResubscribeTests.java +++ b/src/test/java/org/springframework/data/redis/listener/PubSubResubscribeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -236,10 +236,10 @@ private static boolean clusterAvailable() { private static boolean isClusterAware(RedisConnectionFactory connectionFactory) { - if (connectionFactory instanceof LettuceConnectionFactory) { - return ((LettuceConnectionFactory) connectionFactory).isClusterAware(); - } else if (connectionFactory instanceof JedisConnectionFactory) { - return ((JedisConnectionFactory) connectionFactory).isRedisClusterAware(); + if (connectionFactory instanceof LettuceConnectionFactory lettuceConnectionFactory) { + return lettuceConnectionFactory.isClusterAware(); + } else if (connectionFactory instanceof JedisConnectionFactory jedisConnectionFactory) { + return jedisConnectionFactory.isRedisClusterAware(); } return false; } diff --git a/src/test/java/org/springframework/data/redis/listener/PubSubTestParams.java b/src/test/java/org/springframework/data/redis/listener/PubSubTestParams.java index 78d03c6b1f..b034764d48 100644 --- a/src/test/java/org/springframework/data/redis/listener/PubSubTestParams.java +++ b/src/test/java/org/springframework/data/redis/listener/PubSubTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/listener/PubSubTests.java b/src/test/java/org/springframework/data/redis/listener/PubSubTests.java index 4f577dd447..1e0fb72dbb 100644 --- a/src/test/java/org/springframework/data/redis/listener/PubSubTests.java +++ b/src/test/java/org/springframework/data/redis/listener/PubSubTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -170,10 +170,10 @@ void testStartListenersToNoSpecificChannelTest() { private static boolean isClusterAware(RedisConnectionFactory connectionFactory) { - if (connectionFactory instanceof LettuceConnectionFactory) { - return ((LettuceConnectionFactory) connectionFactory).isClusterAware(); - } else if (connectionFactory instanceof JedisConnectionFactory) { - return ((JedisConnectionFactory) connectionFactory).isRedisClusterAware(); + if (connectionFactory instanceof LettuceConnectionFactory lettuce) { + return lettuce.isClusterAware(); + } else if (connectionFactory instanceof JedisConnectionFactory jedis) { + return jedis.isRedisClusterAware(); } return false; } diff --git a/src/test/java/org/springframework/data/redis/listener/ReactiveOperationsTestParams.java b/src/test/java/org/springframework/data/redis/listener/ReactiveOperationsTestParams.java index e1e71a7560..e31b479145 100644 --- a/src/test/java/org/springframework/data/redis/listener/ReactiveOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/listener/ReactiveOperationsTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerIntegrationTests.java index 0e167fc65c..4b59bc872d 100644 --- a/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -23,12 +23,9 @@ import java.nio.ByteBuffer; import java.time.Duration; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.List; -import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingDeque; @@ -110,7 +107,7 @@ void shouldReceiveChannelMessages() { ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory); - container.receiveLater(ChannelTopic.of(CHANNEL1)) // + container.receiveLater(Topic.channel(CHANNEL1)) // .doOnNext(it -> doPublish(CHANNEL1.getBytes(), MESSAGE.getBytes())) // .flatMapMany(Function.identity()) // .as(StepVerifier::create) // @@ -153,7 +150,7 @@ public void onChannelUnsubscribed(byte[] channel, long count) { } }; - container.receive(Collections.singletonList(ChannelTopic.of(CHANNEL1)), listener) // + container.receive(Collections.singletonList(Topic.channel(CHANNEL1)), listener) // .as(StepVerifier::create) // .then(awaitSubscription(container::getActiveSubscriptions)) .then(() -> doPublish(CHANNEL1.getBytes(), MESSAGE.getBytes())) // @@ -220,7 +217,7 @@ public void onPatternUnsubscribed(byte[] pattern, long count) { } }; - container.receive(Collections.singletonList(PatternTopic.of(PATTERN1)), listener) // + container.receive(Collections.singletonList(Topic.pattern(PATTERN1)), listener) // .cast(PatternMessage.class) // .as(StepVerifier::create) // .then(awaitSubscription(container::getActiveSubscriptions)) @@ -314,10 +311,10 @@ void multipleListenShouldTrackSubscriptions() throws Exception { ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory); - Flux> c1 = container.receiveLater(ChannelTopic.of(CHANNEL1)) + Flux> c1 = container.receiveLater(Topic.channel(CHANNEL1)) .block(); Flux> c1p1 = container - .receiveLater(Arrays.asList(ChannelTopic.of(CHANNEL1), PatternTopic.of(PATTERN1)), + .receiveLater(Arrays.asList(Topic.channel(CHANNEL1), PatternTopic.of(PATTERN1)), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.string())) .block(); diff --git a/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerUnitTests.java index 256c5f65dd..f22ff8e560 100644 --- a/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/listener/ReactiveRedisMessageListenerContainerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -79,7 +79,7 @@ void shouldSubscribeToPattern() { container = createContainer(); - container.receive(PatternTopic.of("foo*")).as(StepVerifier::create).thenAwait().thenCancel().verify(); + container.receive(Topic.pattern("foo*")).as(StepVerifier::create).thenAwait().thenCancel().verify(); verify(subscriptionMock).pSubscribe(getByteBuffer("foo*")); } @@ -90,7 +90,7 @@ void shouldSubscribeToMultiplePatterns() { when(subscriptionMock.receive()).thenReturn(Flux.never()); container = createContainer(); - container.receive(PatternTopic.of("foo*"), PatternTopic.of("bar*")).as(StepVerifier::create).thenRequest(1) + container.receive(Topic.pattern("foo*"), Topic.pattern("bar*")).as(StepVerifier::create).thenRequest(1) .thenAwait().thenCancel().verify(); verify(subscriptionMock).pSubscribe(getByteBuffer("foo*"), getByteBuffer("bar*")); @@ -102,7 +102,7 @@ void shouldSubscribeToChannel() { when(subscriptionMock.receive()).thenReturn(Flux.never()); container = createContainer(); - container.receive(ChannelTopic.of("foo")).as(StepVerifier::create).thenAwait().thenCancel().verify(); + container.receive(Topic.channel("foo")).as(StepVerifier::create).thenAwait().thenCancel().verify(); verify(subscriptionMock).subscribe(getByteBuffer("foo")); } @@ -113,7 +113,7 @@ void shouldSubscribeToMultipleChannels() { when(subscriptionMock.receive()).thenReturn(Flux.never()); container = createContainer(); - container.receive(ChannelTopic.of("foo"), ChannelTopic.of("bar")).as(StepVerifier::create).thenAwait().thenCancel() + container.receive(Topic.channel("foo"), Topic.channel("bar")).as(StepVerifier::create).thenAwait().thenCancel() .verify(); verify(subscriptionMock).subscribe(getByteBuffer("foo"), getByteBuffer("bar")); @@ -127,7 +127,7 @@ void shouldEmitChannelMessage() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(ChannelTopic.of("foo")); + Flux> messageStream = container.receive(Topic.channel("foo")); messageStream.as(StepVerifier::create).then(() -> { sink.tryEmitNext(createChannelMessage("foo", "message")); @@ -146,7 +146,7 @@ void shouldEmitPatternMessage() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(PatternTopic.of("foo*")); + Flux> messageStream = container.receive(Topic.pattern("foo*")); messageStream.as(StepVerifier::create).then(() -> { sink.tryEmitNext(createPatternMessage("foo*", "foo", "message")); @@ -171,7 +171,7 @@ void shouldRegisterSubscription() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(ChannelTopic.of("foo*")); + Flux> messageStream = container.receive(Topic.channel("foo*")); Disposable subscription = messageStream.subscribe(); @@ -193,7 +193,7 @@ void shouldRegisterSubscriptionMultipleSubscribers() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(new ChannelTopic("foo*")); + Flux> messageStream = container.receive(Topic.channel("foo*")); Disposable first = messageStream.subscribe(); Disposable second = messageStream.subscribe(); @@ -216,7 +216,7 @@ void shouldUnsubscribeOnCancel() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(PatternTopic.of("foo*")); + Flux> messageStream = container.receive(Topic.pattern("foo*")); messageStream.as(StepVerifier::create).then(() -> { @@ -240,7 +240,7 @@ void shouldTerminateSubscriptionsOnShutdown() { })); container = createContainer(); - Flux> messageStream = container.receive(PatternTopic.of("foo*")); + Flux> messageStream = container.receive(Topic.pattern("foo*")); messageStream.as(StepVerifier::create).then(() -> { container.destroy(); @@ -255,7 +255,7 @@ void shouldCleanupDownstream() { when(subscriptionMock.receive()).thenReturn(sink.asFlux()); container = createContainer(); - Flux> messageStream = container.receive(PatternTopic.of("foo*")); + Flux> messageStream = container.receive(Topic.pattern("foo*")); messageStream.as(StepVerifier::create).then(() -> { assertThat(sink.currentSubscriberCount()).isGreaterThan(0); diff --git a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerFailureIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerFailureIntegrationTests.java index 59b0d9bd52..6ecd8f4b75 100644 --- a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerFailureIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerFailureIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/listener/RedisMessageListenerContainerIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerIntegrationTests.java index 4e4f571476..bea301b046 100644 --- a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/listener/RedisMessageListenerContainerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java index 7d71909c71..ea7dfeb557 100644 --- a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -19,6 +19,7 @@ import static org.mockito.Mockito.*; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; @@ -30,6 +31,7 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.Subscription; @@ -44,6 +46,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Seongjun Lee */ class RedisMessageListenerContainerUnitTests { @@ -221,4 +224,19 @@ void shouldRecoverFromConnectionFailure() throws Exception { void failsOnDuplicateInit() { assertThatIllegalStateException().isThrownBy(() -> container.afterPropertiesSet()); } + + @Test // GH-3009 + void shouldRemoveAllListenersWhenListenerIsNull() { + + MessageListener listener1 = mock(MessageListener.class); + MessageListener listener2 = mock(MessageListener.class); + Topic topic = new ChannelTopic("topic1"); + + container.addMessageListener(listener1, Collections.singletonList(topic)); + container.addMessageListener(listener2, Collections.singletonList(topic)); + + container.removeMessageListener(null, Collections.singletonList(topic)); + + assertThatNoException().isThrownBy(() -> container.removeMessageListener(null, Collections.singletonList(topic))); + } } diff --git a/src/test/java/org/springframework/data/redis/listener/SubscriptionConnectionTests.java b/src/test/java/org/springframework/data/redis/listener/SubscriptionConnectionTests.java index 4d0701a3ea..6237e7e48f 100644 --- a/src/test/java/org/springframework/data/redis/listener/SubscriptionConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/listener/SubscriptionConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/listener/adapter/ContainerXmlSetupIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/adapter/ContainerXmlSetupIntegrationTests.java index ffa2f25f3c..48849ad11a 100644 --- a/src/test/java/org/springframework/data/redis/listener/adapter/ContainerXmlSetupIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/listener/adapter/ContainerXmlSetupIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/listener/adapter/MessageListenerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerUnitTests.java index a8735c93ec..876118bd60 100644 --- a/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/listener/adapter/RedisMDP.java b/src/test/java/org/springframework/data/redis/listener/adapter/RedisMDP.java index 6536e0e79c..63788ffa7a 100644 --- a/src/test/java/org/springframework/data/redis/listener/adapter/RedisMDP.java +++ b/src/test/java/org/springframework/data/redis/listener/adapter/RedisMDP.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/listener/adapter/ThrowableMessageListener.java b/src/test/java/org/springframework/data/redis/listener/adapter/ThrowableMessageListener.java index 0d6465b656..eca3ff4e95 100644 --- a/src/test/java/org/springframework/data/redis/listener/adapter/ThrowableMessageListener.java +++ b/src/test/java/org/springframework/data/redis/listener/adapter/ThrowableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java b/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java index af3cefe216..3807d13680 100644 --- a/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/mapping/BeanUtilsHashMapperTests.java b/src/test/java/org/springframework/data/redis/mapping/BeanUtilsHashMapperTests.java index 94d7488762..301056e3ea 100644 --- a/src/test/java/org/springframework/data/redis/mapping/BeanUtilsHashMapperTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/BeanUtilsHashMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperFlatteningUnitTests.java index 593d7ec594..5e5e2c95cf 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperFlatteningUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/mapping/Jackson2HashMapperIntegrationTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperIntegrationTests.java index 608132504c..8b96474031 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,8 +54,8 @@ public class Jackson2HashMapperIntegrationTests { public Jackson2HashMapperIntegrationTests(RedisConnectionFactory factory) throws Exception { this.factory = factory; - if (factory instanceof InitializingBean) { - ((InitializingBean) factory).afterPropertiesSet(); + if (factory instanceof InitializingBean initializingBean) { + initializingBean.afterPropertiesSet(); } } diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperNonFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperNonFlatteningUnitTests.java index 18c2f2b69b..688058c0ce 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperNonFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperNonFlatteningUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/mapping/Jackson2HashMapperUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperUnitTests.java index 4ca104122f..5b8f392619 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,6 +203,15 @@ void bigDecimalShouldBeTreatedCorrectly() { assertBackAndForwardMapping(source); } + @Test // GH-2979 + void enumsShouldBeTreatedCorrectly() { + + WithEnumValue source = new WithEnumValue(); + source.value = SpringDataEnum.REDIS; + + assertBackAndForwardMapping(source); + } + public static class WithList { List strings; @@ -452,4 +461,38 @@ public int hashCode() { return Objects.hash(getValue()); } } + + enum SpringDataEnum { + COMMONS, REDIS + } + + static class WithEnumValue { + + SpringDataEnum value; + + public SpringDataEnum getValue() { + return value; + } + + public void setValue(SpringDataEnum value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WithEnumValue that = (WithEnumValue) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } } diff --git a/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java b/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java index 31b74f2d57..823338e187 100644 --- a/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/ObjectHashMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/RedisRepositoryClusterIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java index b02fea7953..b8623bb2e9 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/RedisRepositoryIntegrationTestBase.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java index 94009df445..780f630b6b 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/RedisRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java index 120fdc4c96..8437f2ef73 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/CdiExtensionIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java index dfbbffd692..a0b221b5aa 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/CdiExtensionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/Person.java b/src/test/java/org/springframework/data/redis/repository/cdi/Person.java index 8083246095..5a1d07e737 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/Person.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,11 +50,9 @@ public void setName(String name) { public boolean equals(@Nullable Object o) { if (this == o) return true; - if (!(o instanceof Person)) + if (!(o instanceof Person person)) return false; - Person person = (Person) o; - if (id != null ? !id.equals(person.id) : person.id != null) return false; return name != null ? name.equals(person.name) : person.name == null; diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java index 237f08a970..38cc84d46b 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonDB.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/PersonFragment.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragment.java index a5d81e8c00..08fee652ab 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragment.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/PersonFragmentImpl.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragmentImpl.java index 8bfc9c50ab..b17bdc9f3d 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragmentImpl.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonFragmentImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/PersonRepository.java b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java index ac82b1b4fe..4e6a6b6236 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/QualifiedPersonRepository.java b/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java index c04bf3d6bb..5dec512831 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/QualifiedPersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/cdi/RedisCdiDependenciesProducer.java b/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java index 88c310e7e4..67d0053687 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/RedisCdiDependenciesProducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,8 +63,8 @@ public RedisOperations redisOperationsProducerQualified(RedisOpe public void closeRedisOperations(@Disposes RedisOperations redisOperations) throws Exception { - if (redisOperations instanceof DisposableBean) { - ((DisposableBean) redisOperations).destroy(); + if (redisOperations instanceof DisposableBean disposableBean) { + disposableBean.destroy(); } } diff --git a/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java b/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java index 300f37f818..0f9701bc94 100644 --- a/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java +++ b/src/test/java/org/springframework/data/redis/repository/cdi/RepositoryConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/configuration/RedisRepositoriesRegistrarUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrarUnitTests.java new file mode 100644 index 0000000000..be2356227a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoriesRegistrarUnitTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024-2025 the original author 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.redis.repository.configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + */ +class RedisRepositoriesRegistrarUnitTests { + + private BeanDefinitionRegistry registry; + + @BeforeEach + void setUp() { + registry = new DefaultListableBeanFactory(); + } + + @ParameterizedTest // GH-499, GH-3440 + @MethodSource(value = { "args" }) + void configuresRepositoriesCorrectly(AnnotationMetadata metadata, String[] beanNames) { + + RedisRepositoriesRegistrar registrar = new RedisRepositoriesRegistrar(); + registrar.setResourceLoader(new DefaultResourceLoader()); + registrar.setEnvironment(new StandardEnvironment()); + registrar.registerBeanDefinitions(metadata, registry); + + Iterable names = Arrays.asList(registry.getBeanDefinitionNames()); + assertThat(names).contains(beanNames); + } + + static Stream args() { + return Stream.of( + Arguments.of(AnnotationMetadata.introspect(Config.class), + new String[] { "redisRepositoriesRegistrarUnitTests.PersonRepository" }), + Arguments.of(AnnotationMetadata.introspect(ConfigWithBeanNameGenerator.class), + new String[] { "redisRepositoriesRegistrarUnitTests.PersonREPO" })); + } + + @EnableRedisRepositories(basePackageClasses = PersonRepository.class, considerNestedRepositories = true) + private class Config { + + } + + @EnableRedisRepositories(basePackageClasses = PersonRepository.class, nameGenerator = MyBeanNameGenerator.class, + considerNestedRepositories = true) + private class ConfigWithBeanNameGenerator { + + } + + static class MyBeanNameGenerator extends AnnotationBeanNameGenerator { + + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + return super.generateBeanName(definition, registry).replaceAll("Repository", "REPO"); + } + } + + interface PersonRepository extends CrudRepository { + + } + + @RedisHash + static class Person {} +} diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java index 008cb091e3..afc66411de 100644 --- a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author 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,6 +28,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.repository.KeyValueRepository; @@ -46,12 +47,12 @@ */ class RedisRepositoryConfigurationExtensionUnitTests { - private StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + private AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); private ResourceLoader loader = new PathMatchingResourcePatternResolver(); private Environment environment = new StandardEnvironment(); private BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); private RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableRedisRepositories.class, loader, environment, registry); + EnableRedisRepositories.class, loader, environment, registry, null); private RedisRepositoryConfigurationExtension extension = new RedisRepositoryConfigurationExtension(); @@ -75,46 +76,46 @@ void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() { @Test // DATAREDIS-491 void picksUpEnableKeyspaceEventsOnStartupCorrectly() { - metadata = new StandardAnnotationMetadata(Config.class, true); - BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + metadata = AnnotationMetadata.introspect(Config.class); + BeanDefinitionRegistry bdr = getBeanDefinitionRegistry(); - assertThat(getEnableKeyspaceEvents(beanDefintionRegistry)).isEqualTo((Object) EnableKeyspaceEvents.ON_STARTUP); + assertThat(getEnableKeyspaceEvents(bdr)).isEqualTo((Object) EnableKeyspaceEvents.ON_STARTUP); } @Test // DATAREDIS-491 void picksUpEnableKeyspaceEventsDefaultCorrectly() { - metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsDisabled.class, true); - BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + metadata = AnnotationMetadata.introspect(ConfigWithKeyspaceEventsDisabled.class); + BeanDefinitionRegistry bdr = getBeanDefinitionRegistry(); - assertThat(getEnableKeyspaceEvents(beanDefintionRegistry)).isEqualTo((Object) EnableKeyspaceEvents.OFF); + assertThat(getEnableKeyspaceEvents(bdr)).isEqualTo((Object) EnableKeyspaceEvents.OFF); } @Test // DATAREDIS-505 void picksUpDefaultKeyspaceNotificationsConfigParameterCorrectly() { metadata = new StandardAnnotationMetadata(Config.class, true); - BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + BeanDefinitionRegistry bdr = getBeanDefinitionRegistry(); - assertThat(getKeyspaceNotificationsConfigParameter(beanDefintionRegistry)).isEqualTo((Object) "Ex"); + assertThat(getKeyspaceNotificationsConfigParameter(bdr)).isEqualTo((Object) "Ex"); } @Test // DATAREDIS-505 void picksUpCustomKeyspaceNotificationsConfigParameterCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsEnabledAndCustomEventConfig.class, true); - BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + BeanDefinitionRegistry bdr = getBeanDefinitionRegistry(); - assertThat(getKeyspaceNotificationsConfigParameter(beanDefintionRegistry)).isEqualTo((Object) "KEA"); + assertThat(getKeyspaceNotificationsConfigParameter(bdr)).isEqualTo((Object) "KEA"); } @Test // DATAREDIS-1049 void explicitlyEmptyKeyspaceNotificationsConfigParameterShouldBeCapturedCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithEmptyConfigParameter.class, true); - BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + BeanDefinitionRegistry bdr = getBeanDefinitionRegistry(); - assertThat(getKeyspaceNotificationsConfigParameter(beanDefintionRegistry)).isEqualTo(""); + assertThat(getKeyspaceNotificationsConfigParameter(bdr)).isEqualTo(""); } private static void assertDoesNotHaveRepo(Class repositoryInterface, @@ -147,7 +148,7 @@ private BeanDefinitionRegistry getBeanDefinitionRegistry() { BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableRedisRepositories.class, loader, environment, registry); + EnableRedisRepositories.class, loader, environment, registry, null); RedisRepositoryConfigurationExtension extension = new RedisRepositoryConfigurationExtension(); @@ -156,13 +157,13 @@ private BeanDefinitionRegistry getBeanDefinitionRegistry() { return registry; } - private Object getEnableKeyspaceEvents(BeanDefinitionRegistry beanDefintionRegistry) { - return beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() + private Object getEnableKeyspaceEvents(BeanDefinitionRegistry bdr) { + return bdr.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() .getPropertyValue("enableKeyspaceEvents").getValue(); } - private Object getKeyspaceNotificationsConfigParameter(BeanDefinitionRegistry beanDefintionRegistry) { - return beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() + private Object getKeyspaceNotificationsConfigParameter(BeanDefinitionRegistry bdr) { + return bdr.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() .getPropertyValue("keyspaceNotificationsConfigParameter").getValue(); } diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java index b2a21f454f..9ef5506258 100644 --- a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/core/MappingRedisEntityInformationUnitTests.java b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java index 746a7e4cca..6637d6f83e 100644 --- a/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/core/MappingRedisEntityInformationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/query/ExampleQueryMapperUnitTests.java b/src/test/java/org/springframework/data/redis/repository/query/ExampleQueryMapperUnitTests.java index 445beb8f38..9a99d12538 100644 --- a/src/test/java/org/springframework/data/redis/repository/query/ExampleQueryMapperUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/query/ExampleQueryMapperUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java index 8cd54ae12c..c088ff751c 100644 --- a/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/query/RedisQueryCreatorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java index ebcec9b870..0c29bc6e6a 100644 --- a/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/serializer/DefaultRedisElementReaderUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementReaderUnitTests.java index 7be9d472cf..4e9c9759ef 100644 --- a/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementReaderUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementReaderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/serializer/DefaultRedisElementWriterUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementWriterUnitTests.java index 440c45ac04..e971c6dd8b 100644 --- a/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/DefaultRedisElementWriterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index 7f54a324cf..70f7d46615 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author 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,10 @@ */ package org.springframework.data.redis.serializer; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.springframework.test.util.ReflectionTestUtils.getField; -import static org.springframework.util.ObjectUtils.nullSafeEquals; -import static org.springframework.util.ObjectUtils.nullSafeHashCode; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.util.ReflectionTestUtils.*; +import static org.springframework.util.ObjectUtils.*; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -42,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.msgpack.jackson.dataformat.MessagePackFactory; import org.springframework.beans.BeanUtils; import org.springframework.cache.support.NullValue; @@ -51,13 +42,17 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; @@ -74,9 +69,14 @@ class GenericJackson2JsonRedisSerializerUnitTests { private static final SimpleObject SIMPLE_OBJECT = new SimpleObject(1L); private static final ComplexObject COMPLEX_OBJECT = new ComplexObject("steelheart", SIMPLE_OBJECT); - @Test // DATAREDIS-392 + @Test // DATAREDIS-392, GH-2878 void shouldUseDefaultTyping() { assertThat(extractTypeResolver(new GenericJackson2JsonRedisSerializer())).isNotNull(); + assertThat(extractTypeResolver(GenericJackson2JsonRedisSerializer.builder().build())).isNotNull(); + assertThat(extractTypeResolver(GenericJackson2JsonRedisSerializer.builder().defaultTyping(false).build())).isNull(); + assertThat( + extractTypeResolver(GenericJackson2JsonRedisSerializer.builder().objectMapper(new ObjectMapper()).build())) + .isNull(); } @Test // DATAREDIS-392 @@ -98,12 +98,13 @@ void shouldUseDefaultTypingWhenClassPropertyNameIsNull() { @Test // DATAREDIS-392 void shouldUseDefaultTypingWhenClassPropertyNameIsProvided() { - TypeResolverBuilder typeResolver = extractTypeResolver(new GenericJackson2JsonRedisSerializer("firefight")); + TypeResolverBuilder typeResolver = extractTypeResolver( + GenericJackson2JsonRedisSerializer.builder().typeHintPropertyName("firefight").build()); assertThat((String) getField(typeResolver, "_typeProperty")).isEqualTo("firefight"); } @Test // DATAREDIS-392 - void serializeShouldReturnEmptyByteArrayWhenSouceIsNull() { + void serializeShouldReturnEmptyByteArrayWhenSourceIsNull() { assertThat(new GenericJackson2JsonRedisSerializer().serialize(null)).isEqualTo(SerializationUtils.EMPTY_ARRAY); } @@ -227,10 +228,10 @@ void shouldConsiderWriter() { user.id = 42; user.name = "Walter White"; - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null, - JacksonObjectReader.create(), (mapper, source) -> { + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .writer((mapper, source) -> { return mapper.writerWithView(Views.Basic.class).writeValueAsBytes(source); - }); + }).build(); byte[] result = serializer.serialize(user); @@ -240,10 +241,10 @@ void shouldConsiderWriter() { @Test // GH-2322 void considersWriterForCustomObjectMapper() { - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(new ObjectMapper(), - JacksonObjectReader.create(), (mapper, source) -> { + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .objectMapper(new ObjectMapper()).writer((mapper, source) -> { return mapper.writerWithView(Views.Basic.class).writeValueAsBytes(source); - }); + }).build(); User user = new User(); user.email = "walter@heisenberg.com"; @@ -263,13 +264,14 @@ void shouldConsiderReader() { user.id = 42; user.name = "Walter White"; - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null, + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .reader( (mapper, source, type) -> { if (type.getRawClass() == User.class) { return mapper.readerWithView(Views.Basic.class).forType(type).readValue(source); } return mapper.readValue(source, type); - }, JacksonObjectWriter.create()); + }).build(); byte[] serializedValue = serializer.serialize(user); @@ -420,7 +422,7 @@ void deserializesJavaTimeFrimBytes() { } @Test // GH-2601 - public void internalObjectMapperCustomization() { + void internalObjectMapperCustomization() { GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); @@ -432,13 +434,13 @@ public void internalObjectMapperCustomization() { assertThat(serializer.configure(configurer)).isSameAs(serializer); - verify(mockObjectMapper, times(1)).registerModule(eq(mockModule)); + verify(mockObjectMapper).registerModule(mockModule); verifyNoMoreInteractions(mockObjectMapper); verifyNoInteractions(mockModule); } @Test // GH-2601 - public void configureWithNullConsumerThrowsIllegalArgumentException() { + void configureWithNullConsumerThrowsIllegalArgumentException() { assertThatIllegalArgumentException() .isThrownBy(() -> new GenericJackson2JsonRedisSerializer().configure(null)) @@ -446,6 +448,72 @@ public void configureWithNullConsumerThrowsIllegalArgumentException() { .withNoCause(); } + @Test + void defaultSerializeAndDeserializeNullValueWithBuilderClass() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .objectMapper(new ObjectMapper().enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY)) + .build(); + + serializeAndDeserializeNullValue(serializer); + } + + @Test // GH-2878 + void customNullValueSerializer() { + + StdSerializer nullValueSerializer = new StdSerializer<>(NullValue.class) { + @Override + public void serialize(NullValue nullValue, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeNull(); + } + + @Override + public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, SerializerProvider serializers, + TypeSerializer typeSerializer) throws IOException { + + serialize(value, jsonGenerator, serializers); + } + }; + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .nullValueSerializer(nullValueSerializer) + .build(); + + NullValue nv = BeanUtils.instantiateClass(NullValue.class); + + byte[] serializedValue = serializer.serialize(nv); + assertThat(serializedValue).isNotNull(); + + Object deserializedValue = serializer.deserialize(serializedValue); + assertThat(deserializedValue).isNull(); + } + + @Test // GH-2981 + void defaultSerializeAndDeserializeWithCustomJsonFactory() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .objectMapper( + new ObjectMapper(new MessagePackFactory()).enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY)) + .build(); + + byte[] serializedValue = serializer.serialize(COMPLEX_OBJECT); + + Object deserializedValue = serializer.deserialize(serializedValue, Object.class); + assertThat(deserializedValue).isEqualTo(COMPLEX_OBJECT); + } + + @Test // GH-2981 + void defaultSerializeAndDeserializeNullValueWithBuilderClassAndCustomJsonFactory() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .objectMapper( + new ObjectMapper(new MessagePackFactory()).enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY)) + .build(); + + serializeAndDeserializeNullValue(serializer); + } + private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) { NullValue nv = BeanUtils.instantiateClass(NullValue.class); @@ -457,6 +525,7 @@ private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSer assertThat(deserializedValue).isInstanceOf(NullValue.class); } + @Nullable private TypeResolverBuilder extractTypeResolver(GenericJackson2JsonRedisSerializer serializer) { ObjectMapper mapper = (ObjectMapper) getField(serializer, "mapper"); @@ -572,10 +641,9 @@ public boolean equals(@Nullable Object obj) { if (obj == null) { return false; } - if (!(obj instanceof SimpleObject)) { + if (!(obj instanceof SimpleObject other)) { return false; } - SimpleObject other = (SimpleObject) obj; return nullSafeEquals(this.longValue, other.longValue); } } diff --git a/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java b/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java index dab08d2c0c..da98eff85d 100644 --- a/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/serializer/RedisSerializationContextUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/RedisSerializationContextUnitTests.java index e435f57b6e..0d15d0b23b 100644 --- a/src/test/java/org/springframework/data/redis/serializer/RedisSerializationContextUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/RedisSerializationContextUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/serializer/SerializableDomainClass.java b/src/test/java/org/springframework/data/redis/serializer/SerializableDomainClass.java index 46e5712ba1..f02625ce68 100644 --- a/src/test/java/org/springframework/data/redis/serializer/SerializableDomainClass.java +++ b/src/test/java/org/springframework/data/redis/serializer/SerializableDomainClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under 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/redis/serializer/SimpleRedisSerializerTests.java b/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java index af21b6a1fb..f76a4fa5d6 100644 --- a/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/serializer/StringRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/StringRedisSerializerUnitTests.java index 88c99848a8..58d6fd3f93 100644 --- a/src/test/java/org/springframework/data/redis/serializer/StringRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/StringRedisSerializerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 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/redis/stream/AbstractStreamMessageListenerContainerIntegrationTests.java b/src/test/java/org/springframework/data/redis/stream/AbstractStreamMessageListenerContainerIntegrationTests.java index 8df2d894f9..26318b1448 100644 --- a/src/test/java/org/springframework/data/redis/stream/AbstractStreamMessageListenerContainerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/stream/AbstractStreamMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -15,7 +15,10 @@ */ package org.springframework.data.redis.stream; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.output.NestedMultiOutput; import java.time.Duration; import java.util.Collections; @@ -48,9 +51,6 @@ import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.util.NumberUtils; -import io.lettuce.core.codec.StringCodec; -import io.lettuce.core.output.NestedMultiOutput; - /** * Integration tests for {@link StreamMessageListenerContainer}. * @@ -395,9 +395,9 @@ private int getNumberOfPending(String stream, String group) { RedisConnection connection = connectionFactory.getConnection(); - if (connection instanceof LettuceConnection) { + if (connection instanceof LettuceConnection lettuce) { - String value = ((List) ((LettuceConnection) connectionFactory.getConnection()).execute("XPENDING", + String value = ((List) lettuce.execute("XPENDING", new NestedMultiOutput<>(StringCodec.UTF8), new byte[][] { stream.getBytes(), group.getBytes() })).get(0) .toString(); return NumberUtils.parseNumber(value, Integer.class); diff --git a/src/test/java/org/springframework/data/redis/stream/JedisStreamMessageListenerContainerIntegrationTests.java b/src/test/java/org/springframework/data/redis/stream/JedisStreamMessageListenerContainerIntegrationTests.java index e8c2ed56ec..e6e27f7548 100644 --- a/src/test/java/org/springframework/data/redis/stream/JedisStreamMessageListenerContainerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/stream/JedisStreamMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/stream/LettuceStreamMessageListenerContainerIntegrationTests.java b/src/test/java/org/springframework/data/redis/stream/LettuceStreamMessageListenerContainerIntegrationTests.java index 5a6357fefe..91c6f42c19 100644 --- a/src/test/java/org/springframework/data/redis/stream/LettuceStreamMessageListenerContainerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/stream/LettuceStreamMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/stream/ReadOffsetStrategyUnitTests.java b/src/test/java/org/springframework/data/redis/stream/ReadOffsetStrategyUnitTests.java index dac2fe205f..dfb1e24c7f 100644 --- a/src/test/java/org/springframework/data/redis/stream/ReadOffsetStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/redis/stream/ReadOffsetStrategyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/stream/StreamReceiverIntegrationTests.java b/src/test/java/org/springframework/data/redis/stream/StreamReceiverIntegrationTests.java index 5abe16c2c3..48cc363879 100644 --- a/src/test/java/org/springframework/data/redis/stream/StreamReceiverIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/stream/StreamReceiverIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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. @@ -15,11 +15,13 @@ */ package org.springframework.data.redis.stream; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import java.nio.ByteBuffer; import java.time.Duration; @@ -51,10 +53,6 @@ import org.springframework.data.redis.stream.StreamReceiver.StreamReceiverOptions; import org.springframework.data.redis.test.condition.EnabledOnCommand; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - /** * Integration tests for {@link StreamReceiver}. * @@ -227,13 +225,13 @@ void shouldReceiveAsConsumerGroupMessages() { .consumeNextWith(it -> { assertThat(it.getStream()).isEqualTo("my-stream"); - // assertThat(it.getValue()).containsEntry("key", "value"); - assertThat(it.getValue()).containsValue("value"); + + assertThat(it.getValue().values()).containsAnyOf("value", "value2"); }).consumeNextWith(it -> { assertThat(it.getStream()).isEqualTo("my-stream"); // assertThat(it.getValue()).containsEntry("key2", "value2"); - assertThat(it.getValue()).containsValue("value2"); + assertThat(it.getValue().values()).containsAnyOf("value", "value2"); }) // .thenCancel() // .verify(Duration.ofSeconds(5)); diff --git a/src/test/java/org/springframework/data/redis/support/BoundKeyOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/BoundKeyOperationsIntegrationTests.java index e193f58618..ad510f97e5 100644 --- a/src/test/java/org/springframework/data/redis/support/BoundKeyOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/BoundKeyOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -119,14 +119,14 @@ void testPersist() throws Exception { @SuppressWarnings({ "unchecked", "rawtypes" }) private void populateBoundKey() { - if (keyOps instanceof Collection) { - ((Collection) keyOps).add("dummy"); - } else if (keyOps instanceof Map) { - ((Map) keyOps).put("dummy", "dummy"); - } else if (keyOps instanceof RedisAtomicInteger) { - ((RedisAtomicInteger) keyOps).set(42); - } else if (keyOps instanceof RedisAtomicLong) { - ((RedisAtomicLong) keyOps).set(42L); + if (keyOps instanceof Collection collection) { + collection.add("dummy"); + } else if (keyOps instanceof Map map) { + map.put("dummy", "dummy"); + } else if (keyOps instanceof RedisAtomicInteger atomic) { + atomic.set(42); + } else if (keyOps instanceof RedisAtomicLong atomic) { + atomic.set(42L); } } } diff --git a/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java b/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java index 3624336f2c..89b7270a3d 100644 --- a/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java +++ b/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/atomic/AtomicCountersParam.java b/src/test/java/org/springframework/data/redis/support/atomic/AtomicCountersParam.java index f958c93fce..15393f3cc6 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/AtomicCountersParam.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/AtomicCountersParam.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/atomic/CompareAndSetIntegrationIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/atomic/CompareAndSetIntegrationIntegrationTests.java index 0d4b899cf2..6e436a7365 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/CompareAndSetIntegrationIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/CompareAndSetIntegrationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleIntegrationTests.java index 6a1302c0c9..f8941ffe8a 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/support/atomic/RedisAtomicDoubleUnitTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleUnitTests.java index c504e5fe61..22ed35e76b 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleUnitTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerIntegrationTests.java index cd7a4332bf..d5d9ea8bcc 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/support/atomic/RedisAtomicIntegerUnitTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerUnitTests.java index eeb14ccc2f..3e58c78919 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongIntegrationTests.java index 21e22125a6..21ba2729f4 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 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/redis/support/atomic/RedisAtomicLongUnitTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongUnitTests.java index cd4a349183..1e22396d63 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongUnitTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionIntegrationTests.java index 5f6b5ca85b..e4a0cbcd8e 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionUnitTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionUnitTests.java index 47f75e4576..6fe195d0fc 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisCollectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 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/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java index 1306f0d795..692a967c1f 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java index 3f4e17d439..6532cdfdc5 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -20,24 +20,31 @@ import java.io.IOException; import java.text.DecimalFormat; +import java.time.Duration; +import java.time.Instant; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.BeforeEach; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.DoubleAsStringObjectFactory; import org.springframework.data.redis.LongAsStringObjectFactory; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ExpireChanges; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; @@ -190,6 +197,44 @@ void testIncrement() { assertThat(map.increment(k1, 10)).isEqualTo(Long.valueOf(Long.valueOf((String) v1) + 10)); } + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpire() { + + K k1 = getKey(); + V v1 = getValue(); + assertThat(map.put(k1, v1)).isEqualTo(null); + + BoundHashFieldExpirationOperations ops = map.hashFieldExpiration(Collections.singletonList(k1)); + assertThat(ops.expire(Duration.ofSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { + assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); + }); + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { + assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); + }); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); + } + + @ParameterizedRedisTest // GH-3054 + @EnabledOnCommand("HEXPIRE") + void testExpireAt() { + + K k1 = getKey(); + V v1 = getValue(); + assertThat(map.put(k1, v1)).isEqualTo(null); + + BoundHashFieldExpirationOperations ops = map.hashFieldExpiration(Collections.singletonList(k1)); + assertThat(ops.expireAt(Instant.now().plusSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { + assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); + }); + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { + assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); + }); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); + } + @ParameterizedRedisTest void testIncrementDouble() { assumeThat(valueFactory instanceof DoubleAsStringObjectFactory).isTrue(); @@ -496,4 +541,5 @@ public void randomEntryFromHash() { assertThat(map.randomEntry()).isIn(new AbstractMap.SimpleImmutableEntry(k1, v1), new AbstractMap.SimpleImmutableEntry(k2, v2)); } + } diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisSetIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisSetIntegrationTests.java index 60e839548e..8319ee373a 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisSetIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisSetIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java index 20e647c45f..9f443dc7ff 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java b/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java index 3ea95932a5..8b9063497f 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java +++ b/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/DefaultRedisMapUnitUnitTests.java b/src/test/java/org/springframework/data/redis/support/collections/DefaultRedisMapUnitUnitTests.java index f946511750..637a4a690d 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/DefaultRedisMapUnitUnitTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/DefaultRedisMapUnitUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBeanTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBeanTests.java index 4075821b4e..ecff984147 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBeanTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java index 9102a0f1a8..bceca49729 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/RedisMapIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisMapIntegrationTests.java index d6ac4b7911..b54c1ba7ec 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisMapIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisMapIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java index 9f4fd59b7f..be3e627a28 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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. @@ -185,12 +185,6 @@ void testStringPropertyNames() throws Exception { assertThat(keys.contains(key3)).isTrue(); } - @ParameterizedRedisTest - @Override - public void testScanWorksCorrectly() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> super.testScanWorksCorrectly()); - } - // DATAREDIS-241 public static Collection testParams() { diff --git a/src/test/java/org/springframework/data/redis/support/collections/RedisSetIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisSetIntegrationTests.java index cde7e8d8dc..6a0228d346 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisSetIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisSetIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/RedisZSetIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisZSetIntegrationTests.java index 86b3fa4a50..d508e0af78 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisZSetIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisZSetIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/support/collections/SupportXmlIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/SupportXmlIntegrationTests.java index 656b8966d9..d9857db8e5 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/SupportXmlIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/SupportXmlIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2024 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/test/java/org/springframework/data/redis/test/XstreamOxmSerializerSingleton.java b/src/test/java/org/springframework/data/redis/test/XstreamOxmSerializerSingleton.java index ee03b5711c..c22b4216af 100644 --- a/src/test/java/org/springframework/data/redis/test/XstreamOxmSerializerSingleton.java +++ b/src/test/java/org/springframework/data/redis/test/XstreamOxmSerializerSingleton.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledIfLongRunningTest.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledIfLongRunningTest.java index 436be6983a..cee6fede5e 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledIfLongRunningTest.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledIfLongRunningTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnCommand.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommand.java index 346c359987..a9db6d17f5 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommand.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnCommandCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommandCondition.java index 9d5887ec63..1844acbb8a 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommandCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnCommandCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisAvailable.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailable.java index a5f3c31411..0d396cf41f 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailable.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailable.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisAvailableCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailableCondition.java index f1c73953df..b7ec1f82d9 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailableCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisAvailableCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -57,10 +57,10 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con socket.connect(new InetSocketAddress(SettingsUtils.getHost(), annotation.value()), 100); - return enabled(String.format("Connection successful to Redis at %s:%d", SettingsUtils.getHost(), + return enabled("Connection successful to Redis at %s:%d".formatted(SettingsUtils.getHost(), annotation.value())); } catch (IOException ex) { - return disabled(String.format("Cannot connect to Redis at %s:%d (%s)", SettingsUtils.getHost(), + return disabled("Cannot connect to Redis at %s:%d (%s)".formatted(SettingsUtils.getHost(), annotation.value(), ex)); } } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterAvailable.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterAvailable.java index feebd4ad6d..f25cd153e0 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterAvailable.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterAvailable.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisClusterCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterCondition.java index f67cf4d3f8..fe30380184 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisClusterCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -48,11 +48,11 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con } if (RedisDetector.isClusterAvailable()) { - return enabled(String.format("Connection successful to Redis Cluster at %s:%d", SettingsUtils.getHost(), + return enabled("Connection successful to Redis Cluster at %s:%d".formatted(SettingsUtils.getHost(), SettingsUtils.getClusterPort())); } - return disabled(String.format("Cannot connect to Redis Cluster at %s:%d", SettingsUtils.getHost(), + return disabled("Cannot connect to Redis Cluster at %s:%d".formatted(SettingsUtils.getHost(), SettingsUtils.getClusterPort())); } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriver.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriver.java index c904ae9543..7e2da2bef0 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriver.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriver.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisDriverCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java index d4fbf0e78b..68286b5e86 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -81,13 +81,12 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con } if (!foundMatch) { - return disabled(String.format("Driver %s not supported; Supported driver(s): %s", - formatUnsupportedDriver(value), - Arrays.toString(annotation.value()))); + return disabled("Driver %s not supported; Supported driver(s): %s" + .formatted(formatUnsupportedDriver(value), Arrays.toString(annotation.value()))); } } - return enabled("Found enabled driver(s): " + Arrays.toString(annotation.value())); + return enabled("Found enabled driver(s): %s".formatted(Arrays.toString(annotation.value()))); } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelAvailable.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelAvailable.java index b2f225026f..42d29b9a11 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelAvailable.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelAvailable.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisSentinelCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelCondition.java index 9a59d80ba8..26b2665d4d 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisSentinelCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -52,12 +52,11 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con if (RedisDetector.canConnectToPort(annotation.value())) { - return enabled(String.format("Connection successful to Redis Sentinel at %s:%d", SettingsUtils.getHost(), + return enabled("Connection successful to Redis Sentinel at %s:%d".formatted(SettingsUtils.getHost(), annotation.value())); } - return disabled( - String.format("Cannot connect to Redis Sentinel at %s:%d", SettingsUtils.getHost(), annotation.value())); - + return disabled("Cannot connect to Redis Sentinel at %s:%d".formatted(SettingsUtils.getHost(), + annotation.value())); } } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersion.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersion.java index e3c6ab3d67..326e27be64 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersion.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/EnabledOnRedisVersionCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersionCondition.java index f23bfd11aa..5aec897431 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersionCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisVersionCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -61,11 +61,14 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con } }, RedisConditions.class); - boolean versionMet = conditions.hasVersionGreaterOrEqualsTo(requiredVersion); - return versionMet - ? enabled( - String.format("Enabled on version %s (actual version: %s)", requiredVersion, conditions.getRedisVersion())) - : disabled(String.format("Disabled, version %s not available on Redis version %s", requiredVersion, - conditions.getRedisVersion())); + boolean requiredVersionMet = conditions.hasVersionGreaterOrEqualsTo(requiredVersion); + + if (requiredVersionMet) { + return enabled("Enabled on version %s; actual version: %s".formatted(requiredVersion, + conditions.getRedisVersion())); + } + + return disabled("Disabled; version %s not available on Redis version %s".formatted(requiredVersion, + conditions.getRedisVersion())); } } diff --git a/src/test/java/org/springframework/data/redis/test/condition/LongRunningTest.java b/src/test/java/org/springframework/data/redis/test/condition/LongRunningTest.java index ce1495bcf0..2114fa1bbc 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/LongRunningTest.java +++ b/src/test/java/org/springframework/data/redis/test/condition/LongRunningTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/RedisConditions.java b/src/test/java/org/springframework/data/redis/test/condition/RedisConditions.java index 26c5d1d29c..77ce6bf311 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/RedisConditions.java +++ b/src/test/java/org/springframework/data/redis/test/condition/RedisConditions.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -65,7 +65,7 @@ private RedisConditions(RedisClusterCommands commands) { /** * Create {@link RedisCommands} given {@link StatefulRedisConnection}. * - * @param connection must not be {@code null}. + * @param connection must not be {@literal null}. * @return */ public static RedisConditions of(StatefulRedisConnection connection) { @@ -75,7 +75,7 @@ public static RedisConditions of(StatefulRedisConnection connect /** * Create {@link RedisCommands} given {@link StatefulRedisClusterConnection}. * - * @param connection must not be {@code null}. + * @param connection must not be {@literal null}. * @return */ public static RedisConditions of(StatefulRedisClusterConnection connection) { @@ -85,7 +85,7 @@ public static RedisConditions of(StatefulRedisClusterConnection /** * Create {@link RedisConditions} given {@link RedisCommands}. * - * @param commands must not be {@code null}. + * @param commands must not be {@literal null}. * @return */ public static RedisConditions of(RedisClusterCommands commands) { diff --git a/src/test/java/org/springframework/data/redis/test/condition/RedisDetector.java b/src/test/java/org/springframework/data/redis/test/condition/RedisDetector.java index 5195c3f28a..d0f3e50fcf 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/RedisDetector.java +++ b/src/test/java/org/springframework/data/redis/test/condition/RedisDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/condition/RedisDriver.java b/src/test/java/org/springframework/data/redis/test/condition/RedisDriver.java index 6c0240275f..6c73e50fb0 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/RedisDriver.java +++ b/src/test/java/org/springframework/data/redis/test/condition/RedisDriver.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/JedisExtension.java b/src/test/java/org/springframework/data/redis/test/extension/JedisExtension.java index cd77b39ee4..cdf9225e8b 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/JedisExtension.java +++ b/src/test/java/org/springframework/data/redis/test/extension/JedisExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/LettuceExtension.java b/src/test/java/org/springframework/data/redis/test/extension/LettuceExtension.java index a87cb5f1dc..949a0fba7c 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/LettuceExtension.java +++ b/src/test/java/org/springframework/data/redis/test/extension/LettuceExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/LettuceTestClientResources.java b/src/test/java/org/springframework/data/redis/test/extension/LettuceTestClientResources.java index 8cdc9be407..90a57e8a3a 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/LettuceTestClientResources.java +++ b/src/test/java/org/springframework/data/redis/test/extension/LettuceTestClientResources.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/RedisCluster.java b/src/test/java/org/springframework/data/redis/test/extension/RedisCluster.java index 10c2c236a9..5af80603ce 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/RedisCluster.java +++ b/src/test/java/org/springframework/data/redis/test/extension/RedisCluster.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/RedisSentinel.java b/src/test/java/org/springframework/data/redis/test/extension/RedisSentinel.java index c589838987..85218ddb5b 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/RedisSentinel.java +++ b/src/test/java/org/springframework/data/redis/test/extension/RedisSentinel.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/RedisStanalone.java b/src/test/java/org/springframework/data/redis/test/extension/RedisStanalone.java index 625b518465..e04679eb94 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/RedisStanalone.java +++ b/src/test/java/org/springframework/data/redis/test/extension/RedisStanalone.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/ShutdownQueue.java b/src/test/java/org/springframework/data/redis/test/extension/ShutdownQueue.java index 8f8d760351..f818e17e55 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/ShutdownQueue.java +++ b/src/test/java/org/springframework/data/redis/test/extension/ShutdownQueue.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/MethodArgumentsProvider.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodArgumentsProvider.java index ed3b602908..896192fc83 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodArgumentsProvider.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodArgumentsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/MethodSource.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodSource.java index 4c9bc494b9..38e4477242 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodSource.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/MethodSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/ParameterizedRedisTest.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTest.java index 9f80af0bc8..efdef385d3 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTest.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/ParameterizedRedisTestExtension.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTestExtension.java index 7b89c271bb..0e295527ed 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTestExtension.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedRedisTestExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -61,10 +61,11 @@ public boolean supportsTestTemplate(ExtensionContext context) { ParameterizedTestContext methodContext = new ParameterizedTestContext(testMethod); ParameterizedTestContext constructorContext = new ParameterizedTestContext(declaredConstructor); - Preconditions.condition(methodContext.hasPotentiallyValidSignature(), - () -> String.format("@ParameterizedRedisTest method [%s] declares formal parameters in an invalid order: " + Preconditions.condition(methodContext.hasPotentiallyValidSignature(), () -> + ("@ParameterizedRedisTest method [%s] declares formal parameters in an invalid order: " + "argument aggregators must be declared after any indexed arguments " - + "and before any arguments resolved by another ParameterResolver.", testMethod.toGenericString())); + + "and before any arguments resolved by another ParameterResolver.") + .formatted(testMethod.toGenericString())); getStore(context).put(METHOD_CONTEXT_KEY, methodContext); getStore(context).put(CONSTRUCTOR_CONTEXT_KEY, constructorContext); @@ -115,10 +116,9 @@ private ArgumentsProvider instantiateArgumentsProvider(Class String.format( - "Configuration error: @ParameterizedRedisTest on method [%s] must be declared with a non-empty name.", - templateMethod)); + + String pattern = Preconditions.notBlank(parameterizedTest.name().trim(), () -> + "Configuration error: @ParameterizedRedisTest on method [%s] must be declared with a non-empty name" + .formatted(templateMethod)); + return new ParameterizedTestNameFormatter(pattern, displayName, methodContext, argumentMaxLength); } diff --git a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestContext.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestContext.java index af4e61eb18..6d1dd069f1 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestContext.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/ParameterizedTestInvocationContext.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestInvocationContext.java index 5e3b365405..658cd29b20 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestInvocationContext.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestInvocationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/extension/parametrized/ParameterizedTestNameFormatter.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestNameFormatter.java index d67c7399ac..7c02a0171f 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestNameFormatter.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestNameFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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. @@ -53,9 +53,8 @@ String format(int invocationIndex, Object... arguments) { try { return formatSafely(invocationIndex, arguments); } catch (Exception ex) { - String message = "The display name pattern defined for the parameterized test is invalid; " - + "See nested exception for further details."; - throw new JUnitException(message, ex); + throw new JUnitException("The display name pattern defined for the parameterized test is invalid;" + + " See nested exception for further details.", ex); } } diff --git a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestParameterResolver.java b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestParameterResolver.java index 4a644c744b..adcff56f32 100644 --- a/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestParameterResolver.java +++ b/src/test/java/org/springframework/data/redis/test/extension/parametrized/ParameterizedTestParameterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 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/redis/test/util/CollectionAwareComparator.java b/src/test/java/org/springframework/data/redis/test/util/CollectionAwareComparator.java index b150a16a72..c505f89375 100644 --- a/src/test/java/org/springframework/data/redis/test/util/CollectionAwareComparator.java +++ b/src/test/java/org/springframework/data/redis/test/util/CollectionAwareComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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. @@ -34,10 +34,7 @@ public enum CollectionAwareComparator implements Comparator { @Override public int compare(Object o1, Object o2) { - if (o1 instanceof Collection && o2 instanceof Collection) { - - Collection c1 = (Collection) o1; - Collection c2 = (Collection) o2; + if (o1 instanceof Collection c1 && o2 instanceof Collection c2) { if (c1.size() != c2.size()) { return 1; diff --git a/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java b/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java index f6401d6ee5..388d877fdf 100644 --- a/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java +++ b/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 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/test/java/org/springframework/data/redis/test/util/MockitoUtils.java b/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java index 5a384bb838..24d5eb41b3 100644 --- a/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java +++ b/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ public boolean matches(Invocation actual) { @Override public String toString() { - return String.format("%s for method: %s", mode, method); + return "%s for method: %s".formatted(mode, method); } })); } diff --git a/src/test/java/org/springframework/data/redis/test/util/RedisTestData.java b/src/test/java/org/springframework/data/redis/test/util/RedisTestData.java index c0ca6e6f8d..959f0da9bb 100644 --- a/src/test/java/org/springframework/data/redis/test/util/RedisTestData.java +++ b/src/test/java/org/springframework/data/redis/test/util/RedisTestData.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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. @@ -86,7 +86,7 @@ public RedisBucketAssert containingTypeHint(String path, Class type) { isNotNull(); - String hint = String.format("Type hint for <%s> at <%s>", type.getName(), path); + String hint = "Type hint for <%s> at <%s>".formatted(type.getName(), path); if (!actual.containsKey(path)) { diff --git a/src/test/java/org/springframework/data/redis/util/ByteUtilsUnitTests.java b/src/test/java/org/springframework/data/redis/util/ByteUtilsUnitTests.java index 068c0ea716..6a60dac053 100644 --- a/src/test/java/org/springframework/data/redis/util/ByteUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/util/ByteUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 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/test/java/org/springframework/data/redis/util/ConnectionVerifier.java b/src/test/java/org/springframework/data/redis/util/ConnectionVerifier.java index 32dc8dc89b..90af8cb259 100644 --- a/src/test/java/org/springframework/data/redis/util/ConnectionVerifier.java +++ b/src/test/java/org/springframework/data/redis/util/ConnectionVerifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under 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/redis/util/RedisAssertionsUnitTests.java b/src/test/java/org/springframework/data/redis/util/RedisAssertionsUnitTests.java deleted file mode 100644 index d9fbe94d1c..0000000000 --- a/src/test/java/org/springframework/data/redis/util/RedisAssertionsUnitTests.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2017-2024 the original author 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.redis.util; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.dao.InvalidDataAccessApiUsageException; - -/** - * Unit Tests for {@link RedisAssertions}. - * - * @author John Blum - */ -@ExtendWith(MockitoExtension.class) -class RedisAssertionsUnitTests { - - @Mock - private Supplier mockSupplier; - - @Test - void requireNonNullWithMessageAndArgumentsIsSuccessful() { - assertThat(RedisAssertions.requireNonNull("test", "Test message")).isEqualTo("test"); - } - - @Test - void requireNonNullWithMessageAndArgumentsThrowsIllegalArgumentException() { - - assertThatIllegalArgumentException() - .isThrownBy(() -> RedisAssertions.requireNonNull(null, "This is a %s", "test")) - .withMessage("This is a test") - .withNoCause(); - } - - @Test - void requireNonNullWithSupplierIsSuccessful() { - - assertThat(RedisAssertions.requireNonNull("mock", this.mockSupplier)).isEqualTo("mock"); - - verifyNoInteractions(this.mockSupplier); - } - - @Test - void requireNonNullWithSupplierThrowsIllegalArgumentException() { - - doReturn("Mock message").when(this.mockSupplier).get(); - - assertThatIllegalArgumentException() - .isThrownBy(() -> RedisAssertions.requireNonNull(null, this.mockSupplier)) - .withMessage("Mock message") - .withNoCause(); - - verify(this.mockSupplier, times(1)).get(); - verifyNoMoreInteractions(this.mockSupplier); - } - - @Test - void requireNonNullWithRuntimeExceptionSupplierIsSuccessful() { - - assertThat(RedisAssertions.requireNonNull("mock", () -> new InvalidDataAccessApiUsageException("TEST"))) - .isEqualTo("mock"); - } - - @Test - @SuppressWarnings("all") - void requireNonNullWithThrowsRuntimeException() { - - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> RedisAssertions.requireNonNull(null, - () -> new InvalidDataAccessApiUsageException("TEST"))) - .withMessage("TEST") - .withNoCause(); - } - - @Test - void requireStateWithMessageAndArgumentsIsSuccessful() { - assertThat(RedisAssertions.requireState("test", "Mock message")).isEqualTo("test"); - } - - @Test - void requireStateWithMessageAndArgumentsThrowsIllegalStateException() { - - assertThatIllegalStateException() - .isThrownBy(() -> RedisAssertions.requireState(null, "This is a %s", "test")) - .withMessage("This is a test") - .withNoCause(); - } - - @Test - void requireStateWithSupplierIsSuccessful() { - - assertThat(RedisAssertions.requireState("test", this.mockSupplier)).isEqualTo("test"); - - verifyNoInteractions(this.mockSupplier); - } - - @Test - void requiredStateWithSupplierThrowsIllegalStateException() { - - doReturn("Mock message").when(this.mockSupplier).get(); - - assertThatIllegalStateException() - .isThrownBy(() -> RedisAssertions.requireState(null, this.mockSupplier)) - .withMessage("Mock message") - .withNoCause(); - - verify(this.mockSupplier, times(1)).get(); - verifyNoMoreInteractions(this.mockSupplier); - } -} diff --git a/src/test/kotlin/org/springframework/data/redis/core/PartialUpdateExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/PartialUpdateExtensionsUnitTests.kt index eb89fb123f..846cf66c04 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/PartialUpdateExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/PartialUpdateExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensionsUnitTests.kt index ce18d3cfee..ccb6737846 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveGeoOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensionsUnitTests.kt index 11a6c62164..61b00cde04 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveHashOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensionsUnitTests.kt index df1535e475..b1dcfe15df 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveHyperLogLogOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensionsUnitTests.kt index e9a2519208..779b44371c 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveListOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt index 970d81faca..90563f411f 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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. @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test import org.springframework.data.redis.connection.DataType import org.springframework.data.redis.connection.ReactiveSubscription import org.springframework.data.redis.core.script.RedisScript -import org.springframework.data.redis.listener.ChannelTopic +import org.springframework.data.redis.listener.Topic import org.springframework.data.redis.serializer.RedisElementReader import org.springframework.data.redis.serializer.RedisElementWriter import reactor.core.publisher.Flux @@ -167,8 +167,8 @@ class ReactiveRedisOperationsExtensionsUnitTests { @Test // DATAREDIS-1033 fun listenTo() { - val topic1 = ChannelTopic.of("foo") - val topic2 = ChannelTopic.of("bar") + val topic1 = Topic.channel("foo") + val topic2 = Topic.channel("bar") val message = ReactiveSubscription.ChannelMessage("a", "b") val operations = mockk>() every { operations.listenTo(any(), any()) } returns Flux.just(message) diff --git a/src/test/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensionsUnitTests.kt index 30af755677..f7ec83678d 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveSetOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensionsUnitTests.kt index 9e14277fb4..22ea0603af 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveStreamOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensionsUnitTests.kt index 748ff04a05..50ad68e9d2 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveValueOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensionsUnitTests.kt index 9f7f52175f..91c75933ba 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 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/test/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensionsUnitTests.kt index 219c1ace31..dc40354c98 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/script/RedisScriptExtensionsUnitTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 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.